diff --git a/FlowThread_body.php b/FlowThread_body.php index 8e3337e..1f11fcc 100755 --- a/FlowThread_body.php +++ b/FlowThread_body.php @@ -64,15 +64,20 @@ public static function onBeforePageDisplay(OutputPage &$output, Skin &$skin) { $output->addJsConfigVars(array('commentadmin' => '')); } + global $wgFlowThreadConfig; + $config = array( + 'Avatar' => $wgFlowThreadConfig['Avatar'], + 'AnonymousAvatar' => $wgFlowThreadConfig['AnonymousAvatar'], + ); + if (\FlowThread\Post::canPost($output->getUser())) { $output->addJsConfigVars(array('canpost' => '')); + } else { + $config['CantPostNotice'] = wfMessage('flowthread-ui-cantpost')->toString(); } global $wgFlowThreadConfig; - $output->addJsConfigVars(array('wgFlowThreadConfig' => array( - 'Avatar' => $wgFlowThreadConfig['Avatar'], - 'AnonymousAvatar' => $wgFlowThreadConfig['AnonymousAvatar'], - ))); + $output->addJsConfigVars(array('wgFlowThreadConfig' => $config)); $output->addModules('ext.flowthread'); return true; } diff --git a/assets/common.js b/assets/common.js new file mode 100755 index 0000000..e32080d --- /dev/null +++ b/assets/common.js @@ -0,0 +1,164 @@ +var config = mw.config.get('wgFlowThreadConfig'); + +/* Get avatar by user name */ +function getAvatar(id, username) { + if(id===0) { + return config.AnonymousAvatar; + }else{ + return config.Avatar.replace(/\$\{username\}/g, username); + } +} + +/* Get user friendly time string (such as 1 hour age) */ +function getTimeString(time) { + var m = moment(time).locale(mw.config.get('wgUserLanguage')); + var diff = Date.now() - time; + if (0 < diff && diff < 24 * 3600 * 1000) { + return m.fromNow(); + } else { + return m.format('LL, HH:mm:ss'); + } +} + +function Thread() { + var template = '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '' + + '
'; + + var object = $(template); + + this.post = null; + this.object = object; + $.data(object[0], 'thread', this); +} + +Thread.fromId = function(id) { + return $.data($('[comment-id=' + id + ']')[0], 'thread'); +} + +Thread.prototype.init = function(post) { + var object = this.object; + this.post = post; + object.attr('comment-id', post.id); + + var userlink; + if (post.userid !== 0) { + userlink = wrapPageLink('User:' + post.username, post.username); + } else { + userlink = wrapText(post.username); + } + object.find('.comment-user').html(userlink); + object.find('.comment-avatar img').attr('src', getAvatar(post.userid, post.username)); + object.find('.comment-text').html(post.text); + object.find('.comment-time') + .text(getTimeString(post.timestamp * 1000)) + .siblings().remove(); // Remove all button after init +} + +Thread.prototype.addButton = function(type, text, listener) { + return $('') + .addClass('comment-' + type) + .text(text) + .click(listener) + .appendTo(this.object.find('.comment-footer')); +} + +Thread.prototype.appendChild = function(thread) { + this.object.append(thread.object); +} + +Thread.prototype.prependChild = function(thread) { + this.object.children('.comment-post').after(thread.object); +} +function wrapText(text) { + var span = $(''); + span.text(text); + return span.wrapAll('
').parent().html(); +} + +function wrapPageLink(page, name) { + var link = $(''); + link.attr('href', mw.util.getUrl(page)); + link.text(name); + return link.wrapAll('
').parent().html(); +} + +Thread.prototype.like = function() { + var api = new mw.Api(); + api.get({ + action: 'flowthread', + type: 'like', + postid: this.post.id + }); + this.object.find('.comment-like').first().attr('liked', ''); + this.object.find('.comment-report').first().removeAttr('reported'); +} + +Thread.prototype.dislike = function() { + var api = new mw.Api(); + api.get({ + action: 'flowthread', + type: 'dislike', + postid: this.post.id + }); + this.object.find('.comment-like').first().removeAttr('liked'); + this.object.find('.comment-report').first().removeAttr('reported'); +} + +Thread.prototype.report = function() { + var api = new mw.Api(); + api.get({ + action: 'flowthread', + type: 'report', + postid: this.post.id + }); + this.object.find('.comment-like').first().removeAttr('liked'); + this.object.find('.comment-report').first().attr('reported', ''); +} + +Thread.prototype.delete = function() { + var api = new mw.Api(); + api.get({ + action: 'flowthread', + type: 'delete', + postid: this.post.id + }); + this.object.remove(); +} + +function ReplyBox() { + var template = '
' + + '
' + + '' + + '
' + + '
' + + '' + + '
' + + '' + + mw.msg('flowthread-ui-usewikitext') + + '' + + '
' + + '
'; + + var self = this; + var object = $(template); + this.object = object; + + object.find('textarea').keyup(function(e) { + if (e.ctrlKey && e.which === 13) object.find('.comment-submit').click(); + self.pack(); + }); +} + +ReplyBox.prototype.pack = function() { + var textarea = this.object.find('textarea'); + textarea.height(1).height(textarea[0].scrollHeight); +} \ No newline at end of file diff --git a/css/flowthread.css b/css/flowthread.css index 2bed6fc..27a433c 100755 --- a/css/flowthread.css +++ b/css/flowthread.css @@ -175,12 +175,12 @@ display: none; } -.comment-post:hover .comment-report, .comment-report[reported], .comment-post:hover .comment-delete[enabled] { +.comment-post:hover .comment-report, .comment-report[reported], .comment-post:hover .comment-delete { display: initial; } .comment-thread .comment-replybox:not(:first-child) { - margin-left: 70px; + margin-left: 50px; } .comment-thread .comment-thread { @@ -226,4 +226,12 @@ color: #d32; border: 1px solid #ccc; background-color: rgba(0,0,0,0.03); -} \ No newline at end of file +} + +.comment-bannotice { + border-top: 1px solid rgba(0,0,0,0.13); + font-size: 13px; + text-align: center; + padding: 1em; + color: #777; +} diff --git a/css/manage.css b/css/manage.css index a5e88ec..a6e597f 100755 --- a/css/manage.css +++ b/css/manage.css @@ -93,6 +93,6 @@ } .comment-selected { - padding-left: 2em; + padding-left: 1em; background: lightblue; } \ No newline at end of file diff --git a/extension.json b/extension.json index cc160c7..bca8ee9 100755 --- a/extension.json +++ b/extension.json @@ -3,7 +3,7 @@ "author": "Gary Guo", "url": "https://github.com/nbdd0121/MW-FlowThread", "descriptionmsg": "flowthread_desc", - "version": "1.1.1", + "version": "1.1.2", "license-name": "BSD-2-Clause", "type": "specialpage", "ExtensionMessagesFiles": { @@ -25,7 +25,8 @@ "FlowThread\\UUID": "includes/UUID.php", "FlowThread\\SpamFilter": "includes/SpamFilter.php", "FlowThread\\EchoHook": "/includes/Echo.php", - "FlowThread\\EchoReplyFormatter": "/includes/Echo.php" + "FlowThread\\EchoReplyFormatter": "/includes/Echo.php", + "FlowThread\\Helper": "/includes/Helper.php" }, "Hooks": { "BeforePageDisplay": [ @@ -60,9 +61,11 @@ "ResourceModules": { "ext.flowthread": { "dependencies": [ - "moment" + "moment", + "mediawiki.user" ], "scripts":[ + "assets/common.js", "js/flowthread.js" ], "styles":[ @@ -81,9 +84,11 @@ }, "ext.flowthread.manage": { "dependencies": [ - "moment" + "moment", + "mediawiki.user" ], "scripts": [ + "assets/common.js", "js/manage.js" ], "styles": [ @@ -152,4 +157,4 @@ "echo-subscriptions-email-flowthread": false }, "manifest_version": 1 -} \ No newline at end of file +} diff --git a/i18n/en.json b/i18n/en.json index d3259ef..cde937d 100755 --- a/i18n/en.json +++ b/i18n/en.json @@ -41,6 +41,7 @@ "flowthread-ui-placeholder": "Say Something...", "flowthread-ui-submit": "Submit", "flowthread-ui-nocontent": "Content cannot be empty", + "flowthread-ui-cantpost": "You do not have right to post. Please check site's policy about commenting.", "echo-category-title-flowthread": "FlowThread comment", "echo-pref-tooltip-flowthread": "Notify me when my comment is replyed.", diff --git a/i18n/zh-hans.json b/i18n/zh-hans.json index 8ed8d4b..ebc72b0 100755 --- a/i18n/zh-hans.json +++ b/i18n/zh-hans.json @@ -41,6 +41,7 @@ "flowthread-ui-placeholder": "说点什么吧...", "flowthread-ui-submit": "提交", "flowthread-ui-nocontent": "内容不能为空", + "flowthread-ui-cantpost": "您没有权限发表评论,请查看站点关于评论的政策。", "echo-category-title-flowthread": "FlowThread评论", "echo-pref-tooltip-flowthread": "当我的评论被回复时提醒我。", diff --git a/includes/API.php b/includes/API.php index 4f6b98a..156a003 100755 --- a/includes/API.php +++ b/includes/API.php @@ -7,34 +7,33 @@ private function dieNoParam($name) { $this->dieUsage("The $name parameter must be set", "no$name"); } - private function fetchPosts($pageid) { - $offset = intval($this->getMain()->getVal('offset', 0)); - - $page = new Page($pageid); - $page->type = Post::STATUS_NORMAL; - $page->offset = $offset; - $page->fetch(); - - $comments = array(); - foreach ($page->posts as $post) { - // No longer need to filter out invisible posts - - $favorCount = $post->getFavorCount(); - $myAtt = $post->getUserAttitude($this->getUser()); - - $data = array( + private function convertPosts(array $posts) { + $attTable = Helper::batchGetUserAttitude($this->getUser(), $posts); + $ret = array(); + foreach ($posts as $post) { + $ret[] = array( 'id' => $post->id->getHex(), 'userid' => $post->userid, 'username' => $post->username, 'text' => $post->text, 'timestamp' => $post->id->getTimestamp(), 'parentid' => $post->parentid ? $post->parentid->getHex() : '', - 'like' => $favorCount, - 'myatt' => $myAtt, + 'like' => $post->getFavorCount(), + 'myatt' => $attTable[$post->id->getHex()], ); - - $comments[] = $data; } + return $ret; + } + + private function fetchPosts($pageid) { + $offset = intval($this->getMain()->getVal('offset', 0)); + + $page = new Page($pageid); + $page->type = Post::STATUS_NORMAL; + $page->offset = $offset; + $page->fetch(); + + $comments = $this->convertPosts($page->posts); $obj = array( "posts" => $comments, @@ -59,6 +58,14 @@ private function parsePostList($postList) { return $ret; } + private function executeList() { + $page = $this->getMain()->getVal('pageid'); + if (!$page) { + $this->dieNoParam('pageid'); + } + $this->getResult()->addValue(null, $this->getModuleName(), $this->fetchPosts($page)); + } + public function execute() { $action = $this->getMain()->getVal('type'); $page = $this->getMain()->getVal('pageid'); @@ -71,10 +78,7 @@ public function execute() { switch ($action) { case 'list': - if (!$page) { - $this->dieNoParam('pageid'); - } - $this->getResult()->addValue(null, $this->getModuleName(), $this->fetchPosts($page)); + $this->executeList(); break; case 'like': @@ -149,10 +153,14 @@ public function execute() { // Permission check Post::checkIfCanPost($this->getUser()); - $spam = !SpamFilter::validate($text); + // Need to feed this to spam filter + $useWikitext = $this->getMain()->getCheck('wikitext'); + + $filterResult = SpamFilter::validate($text, $this->getUser(), $useWikitext); + $text = $filterResult['text']; // Parse as wikitext if specified - if ($this->getMain()->getCheck('wikitext')) { + if ($useWikitext) { $parser = new \Parser(); $opt = new \ParserOptions($this->getUser()); $opt->setEditSection(false); @@ -161,8 +169,8 @@ public function execute() { unset($parser); unset($opt); unset($output); - - $text = SpamFilter::badCodeFilter($text); + $text = \Parser::stripOuterParagraph($text); + $text = SpamFilter::sanitize($text); } else { $text = htmlspecialchars($text); } @@ -174,7 +182,7 @@ public function execute() { 'username' => $this->getUser()->getName(), 'text' => $text, 'parentid' => count($postList) ? $postList[0]->id : null, - 'status' => $spam ? Post::STATUS_SPAM : Post::STATUS_NORMAL, + 'status' => $filterResult['good'] ? Post::STATUS_NORMAL : Post::STATUS_SPAM, 'like' => 0, 'report' => 0, ); @@ -190,7 +198,7 @@ public function execute() { $postObject->post(); - if ($spam) { + if (!$filterResult['good']) { global $wgTriggerFlowThreadHooks; if ($wgTriggerFlowThreadHooks) { \Hooks::run('FlowThreadSpammed', array($postObject)); diff --git a/includes/Helper.php b/includes/Helper.php new file mode 100755 index 0000000..d1188f4 --- /dev/null +++ b/includes/Helper.php @@ -0,0 +1,56 @@ +addQuotes($item); + } + return ' IN(' . $range . ')'; + } + + public static function buildPostInExpr(\DatabaseBase $db, array $arr) { + $range = ''; + foreach ($arr as $post) { + if ($range) { + $range .= ','; + } + $range .= $db->addQuotes($post->id->getBin()); + } + return ' IN(' . $range . ')'; + } + + public static function batchGetUserAttitude(\User $user, array $posts) { + if (!count($posts)) { + return array(); + } + + $dbr = wfGetDB(DB_SLAVE); + + $inExpr = self::buildPostInExpr($dbr, $posts); + $res = $dbr->select('FlowThreadAttitude', array( + 'flowthread_att_id', + 'flowthread_att_type', + ), array( + 'flowthread_att_id' . $inExpr, + 'flowthread_att_userid' => $user->getId(), + )); + + $ret = array(); + foreach ($res as $row) { + $ret[UUID::fromBin($row->flowthread_att_id)->getHex()] = intval($row->flowthread_att_type); + } + foreach ($posts as $post) { + if (!isset($ret[$post->id->getHex()])) { + $ret[$post->id->getHex()] = Post::ATTITUDE_NORMAL; + } + } + + return $ret; + } +} \ No newline at end of file diff --git a/includes/Post.php b/includes/Post.php index 2e85f65..365cef4 100755 --- a/includes/Post.php +++ b/includes/Post.php @@ -81,6 +81,7 @@ public static function newFromDatabaseRow(\stdClass $row) { } public static function newFromId(UUID $id) { + // Do not apply cache here, it will seriously slow down the application $dbr = wfGetDB(DB_SLAVE); $row = $dbr->selectRow('FlowThread', @@ -145,6 +146,25 @@ private static function checkIfCanVote(\User $user) { } } + private function publishSimpleLog($subtype, \User $initiator) { + $logEntry = new \ManualLogEntry('comments', $subtype); + $logEntry->setPerformer($initiator); + $logEntry->setTarget(\Title::newFromId($this->pageid)); + $logEntry->setParameters(array( + '4::username' => $this->username, + )); + $logId = $logEntry->insert(); + $logEntry->publish($logId, 'udp'); + } + + private function switchStatus($newStatus) { + $dbw = wfGetDB(DB_MASTER); + $dbw->update('FlowThread', array( + 'flowthread_status' => $newStatus, + ), array( + 'flowthread_id' => $this->id->getBin(), + )); + } public function recover(\User $user) { self::checkIfAdmin($user); @@ -154,22 +174,10 @@ public function recover(\User $user) { } // Mark status as normal - $dbw = wfGetDB(DB_MASTER); - $dbw->update('FlowThread', array( - 'flowthread_status' => static::STATUS_NORMAL, - ), array( - 'flowthread_id' => $this->id->getBin(), - )); + $this->switchStatus(static::STATUS_NORMAL); // Write a log - $logEntry = new \ManualLogEntry('comments', 'recover'); - $logEntry->setPerformer($user); - $logEntry->setTarget(\Title::newFromId($this->pageid)); - $logEntry->setParameters(array( - '4::postid' => $this->username, - )); - $logId = $logEntry->insert(); - $logEntry->publish($logId, 'udp'); + $this->publishSimpleLog('recover', $user); global $wgTriggerFlowThreadHooks; if ($wgTriggerFlowThreadHooks) { @@ -190,22 +198,10 @@ public function delete(\User $user) { } // Mark status as deleted - $dbw = wfGetDB(DB_MASTER); - $dbw->update('FlowThread', array( - 'flowthread_status' => static::STATUS_DELETED, - ), array( - 'flowthread_id' => $this->id->getBin(), - )); + $this->switchStatus(static::STATUS_DELETED); // Write a log - $logEntry = new \ManualLogEntry('comments', 'delete'); - $logEntry->setPerformer($user); - $logEntry->setTarget(\Title::newFromId($this->pageid)); - $logEntry->setParameters(array( - '4::postid' => $this->username, - )); - $logId = $logEntry->insert(); - $logEntry->publish($logId, 'udp'); + $this->publishSimpleLog('delete', $user); global $wgTriggerFlowThreadHooks; if ($wgTriggerFlowThreadHooks) { diff --git a/includes/SpamFilter.php b/includes/SpamFilter.php index 7443a77..24a1340 100644 --- a/includes/SpamFilter.php +++ b/includes/SpamFilter.php @@ -2,23 +2,6 @@ namespace FlowThread; class SpamFilter { - public static function validate($text) { - $blacklist = self::getBlackList(); - - if ($blacklist && preg_match($blacklist, $text)) { - return false; - } - - return true; - } - - public static function badCodeFilter($html) { - return preg_replace('/position(?:\/\*[^*]*\*+([^\/*][^*]*\*+)*\/|\s)*:(?:\/\*[^*]*\*+([^\/*][^*]*\*+)*\/|\s)*fixed/i', '', $html); - } - - private static function stripLines($lines) { - return array_filter(array_map('trim', preg_replace('/#.*$/', '', $lines))); - } private static function validateRegex($regex) { wfSuppressWarnings(); @@ -32,30 +15,161 @@ private static function validateRegex($regex) { return true; } - private static function buildBlacklist() { - $source = wfMessage('flowthread-blacklist')->inContentLanguage(); - if ($source->isDisabled()) { + private static function parseLine($line) { + $line = trim(preg_replace('/#.*$/', '', $line)); // Remove comments and trim space + if (!$line) { + // The line does not contain a regular expression return null; } - $lines = explode("\n", $source->text()); - $lines = self::stripLines($lines); - $lines = array_filter($lines, function ($regex) { - return self::validateRegex('/' . $regex . '/'); - }); - if (!count($lines)) { + + // Extract regex and options from the result + $result = null; + preg_match('/^(.*?)(?:\s*<([^<>]*)>)?$/', $line, $result); + @list($full, $regex, $opts) = $result; + + if (!$line) { + // Cannot contain only options return null; } - return '/' . implode('|', $lines) . '/i'; + + if (!$opts) { + // Default value + $opts = ''; + } + + // Must be a valid regex + // This can also prevent problems when we joining the regex using | + if (!self::validateRegex('/' . $regex . '/')) { + // Abort for invalid regex + return null; + } + + return array( + 'regex' => $regex, + 'opt' => $opts, + ); } - public static function getBlackList() { + private static function parseOptions($opts) { + $options = array(); + $segments = explode('|', $opts); + foreach ($segments as $opt) { + // Extract key=value pair + $exploded = explode('=', $opt, 2); + $key = $exploded[0]; + $value = isset($exploded[1]) ? $exploded[1] : ''; + + switch ($key) { + case 'replace': + // Replace the text instead of marking as spam + $options['replace'] = $value; + break; + default: + if (in_array($key, \User::getAllRights())) { + // If the name is a user right + if (isset($options['right'])) { + $options['right'][] = $key; + } else { + $options['right'] = array($key); + } + } else if (in_array($key, \User::getAllGroups())) { + // If the name is a user group + if (isset($options['group'])) { + $options['group'][] = $key; + } else { + $options['group'] = array($key); + } + } + } + } + return $options; + } + + private static function parseLines($lines) { + $batches = array(); + foreach ($lines as $line) { + $parsed = self::parseLine($line); + if ($parsed) { + if (isset($batches[$parsed['opt']])) { + // Concatenate regexes to speed up + $batches[$parsed['opt']] .= '|' . $parsed['regex']; + } else { + $batches[$parsed['opt']] = $parsed['regex']; + } + } + } + $ret = array(); + foreach ($batches as $opt => $regex) { + $ret[] = array( + '/' . $regex . '/i', + self::parseOptions($opt), + ); + } + return $ret; + } + + private static function getBlackList() { $cache = \ObjectCache::getMainWANInstance(); return $cache->getWithSetCallback( wfMemcKey('flowthread', 'spamblacklist'), 60, function () { - return self::buildBlacklist(); + $source = wfMessage('flowthread-blacklist')->inContentLanguage(); + if ($source->isDisabled()) { + return array(); + } + $lines = explode("\n", $source->text()); + return self::parseLines($lines); } ); } + + public static function validate($text, $poster, $wikitext) { + $blacklist = self::getBlackList(); + $spammed = false; + $ret = array( + 'good' => true, + ); + + foreach ($blacklist as $line) { + list($regex, $opt) = $line; + if (preg_match($regex, $text)) { + if (isset($opt['group'])) { + // When user is in the allowed group list, we skip this rule + if (count(array_intersect($opt['group'], $poster->getGroups()))) { + continue; + } + } + + if (isset($opt['right'])) { + // Right-based control + foreach ($opt['right'] as $item) { + if ($poster->isAllowed($item)) { + continue 2; + } + } + } + + if (isset($opt['replace'])) { + $replaceText = $opt['replace']; + if ($wikitext) { + $replaceText = '' . $replaceText . ''; + } + // Do text replace instead of moving into spam + $text = preg_replace($regex, $replaceText, $text); + continue; + } + + // Mark as bad + $ret['good'] = false; + } + } + + $ret['text'] = $text; + return $ret; + } + + public static function sanitize($html) { + return preg_replace('/position(?:\/\*[^*]*\*+([^\/*][^*]*\*+)*\/|\s)*:(?:\/\*[^*]*\*+([^\/*][^*]*\*+)*\/|\s)*fixed/i', '', $html); + } } \ No newline at end of file diff --git a/js/flowthread.js b/js/flowthread.js index 7949b94..83206c9 100755 --- a/js/flowthread.js +++ b/js/flowthread.js @@ -1,124 +1,41 @@ var canpost = mw.config.exists('canpost'); -var template = '
' - + '
' - + '' - + '
' - + '
' - + '
' - + '
' - + '' - + '
'; -var config = mw.config.get('wgFlowThreadConfig'); - -function getAvatar(id, username) { - if(!config) return ''; - if(id===0) { - return config.AnonymousAvatar; - }else{ - return config.Avatar.replace(/\$\{username\}/g, username); - } -} - -var replyBoxTemplate = '
' - + '
' - + '' - + '
' - + '
' - + '' - + '
' - + '' - + mw.msg('flowthread-ui-usewikitext') - + '' - + '
' - + '
'; - -function getTimeString(time) { - var m = moment(time).locale(mw.config.get('wgUserLanguage')); - var diff = Date.now() - time; - if (0 < diff && diff < 24 * 3600 * 1000) { - return m.fromNow(); - } else { - return m.format('LL, HH:mm:ss'); - } -} - -function wrapText(text) { - var span = $(''); - span.text(text); - return span.wrapAll('
').parent().html(); -} - -function wrapPageLink(page, name) { - var link = $(''); - link.attr('href', mw.util.getUrl(page)); - link.text(name); - return link.wrapAll('
').parent().html(); -} - var replyBox = null; -function Thread(post) { - var self = this; - var object = $(template); - - this.post = post; - this.object = object; - // $.data(object, 'flowthread', this); +function createThread(post) { + var thread = new Thread(); + var object = thread.object; + thread.init(post); - object.attr('comment-id', post.id); - - var userlink; - if (post.userid !== 0) { - userlink = wrapPageLink('User:' + post.username, post.username); - } else { - userlink = wrapText(post.username); + if (canpost) { + thread.addButton('reply', mw.msg('flowthread-ui-reply'), function() { + thread.reply(); + }); } - object.find('.comment-user').html(userlink); - object.find('.comment-avatar img').attr('src', getAvatar(post.userid, post.username)); - object.find('.comment-text').html(post.text); - object.find('.comment-time').text(getTimeString(post.timestamp * 1000)); - object.find('.comment-reply').click(function() { - self.reply(); - }); - object.find('.comment-like').click(function() { - if (object.find('.comment-like').attr('liked') !== undefined) { - self.dislike(post.id); - } else { - self.like(post.id); - } - }); - object.find('.comment-report').click(function() { - if (object.find('.comment-report').attr('reported') !== undefined) { - self.dislike(post.id); - } else { - self.report(post.id); - } - }); - object.find('.comment-delete').click(function() { - self.delete(post.id); - }); + // User not signed in do not have right to vote + if (mw.user.getId() !== 0) { + var likeNum = post.like ? '(' + post.like + ')' : ''; + thread.addButton('like', mw.msg('flowthread-ui-like') + likeNum, function() { + if (object.find('.comment-like').attr('liked') !== undefined) { + thread.dislike(); + } else { + thread.like(); + } + }); + thread.addButton('report', mw.msg('flowthread-ui-report'), function() { + if (object.find('.comment-like').attr('reported') !== undefined) { + thread.dislike(); + } else { + thread.report(); + } + }); + } // commentadmin-restricted and poster himself can delete comment if (mw.config.exists('commentadmin') || (post.userid && post.userid === mw.user.getId())) { - object.find('.comment-delete').attr('enabled', ''); - } - - if (mw.user.getId() === 0) { - object.find('.comment-like, .comment-report').removeAttr('enabled'); + thread.addButton('delete', mw.msg('flowthread-ui-delete'), function() { + thread.delete(); + }); } if (post.myatt === 1) { @@ -126,52 +43,8 @@ function Thread(post) { } else if (post.myatt === 2) { object.find('.comment-report').attr('reported', ''); } - if (post.like !== 0) { - object.find('.comment-like span').text('(' + post.like + ')'); - } -} -Thread.prototype.like = function() { - var api = new mw.Api(); - api.get({ - action: 'flowthread', - type: 'like', - postid: this.post.id - }); - this.object.find('.comment-like').first().attr('liked', ''); - this.object.find('.comment-report').first().removeAttr('reported'); -} - -Thread.prototype.dislike = function() { - var api = new mw.Api(); - api.get({ - action: 'flowthread', - type: 'dislike', - postid: this.post.id - }); - this.object.find('.comment-like').first().removeAttr('liked'); - this.object.find('.comment-report').first().removeAttr('reported'); -} - -Thread.prototype.report = function() { - var api = new mw.Api(); - api.get({ - action: 'flowthread', - type: 'report', - postid: this.post.id - }); - this.object.find('.comment-like').first().removeAttr('liked'); - this.object.find('.comment-report').first().attr('reported', ''); -} - -Thread.prototype.delete = function() { - var api = new mw.Api(); - api.get({ - action: 'flowthread', - type: 'delete', - postid: this.post.id - }); - this.object.remove(); + return thread; } Thread.prototype.reply = function() { @@ -179,7 +52,7 @@ Thread.prototype.reply = function() { replyBox.remove(); } replyBox = createReplyBox(this.post.id); - setFollowUp(this.post.id, replyBox); + this.appendChild({object: replyBox}); } Thread.sendComment = function(postid, text, wikitext) { @@ -209,9 +82,9 @@ function reloadComments(offset) { $('.comment-container').html(''); data.flowthread.posts.forEach(function(item) { if (item.parentid === '') { - $('.comment-container').append(new Thread(item).object); + $('.comment-container').append(createThread(item).object); } else { - setFollowUp(item.parentid, new Thread(item).object); + Thread.fromId(item.parentid).appendChild(createThread(item)); } }); pager.current = Math.floor(offset / 10); @@ -226,15 +99,10 @@ function setFollowUp(postid, follow) { } function createReplyBox(parentid) { - var replyBox = $(replyBoxTemplate); + var replyBox = new ReplyBox().object; var textarea = replyBox.find('textarea'); var submit = replyBox.find('.comment-submit'); var useWikitext = replyBox.find('[name=wikitext]'); - textarea.keyup(function(e) { - if (e.ctrlKey && e.which === 13) submit.click(); - $(this).height(1); - $(this).height(this.scrollHeight); - }); submit.click(function() { var text = textarea.val().trim(); if (!text) { @@ -297,5 +165,10 @@ Paginator.prototype.repaint = function() { var pager = new Paginator(); -$('#bodyContent').after('
', pager.object, canpost ? createReplyBox('') : null); +$('#bodyContent').after('
', pager.object, function(){ + if (canpost) return createReplyBox(''); + var noticeContainer = $('
').addClass('comment-bannotice'); + noticeContainer.html(config.CantPostNotice); + return noticeContainer; +}()); reloadComments(); \ No newline at end of file diff --git a/js/manage.js b/js/manage.js index 28d4f83..8e802d3 100755 --- a/js/manage.js +++ b/js/manage.js @@ -1,113 +1,43 @@ -var template = '
' - + '
' - + '' - + '
' - + '
' - + '
' - + '
' - + '' - + '
'; -var config = mw.config.get('wgFlowThreadConfig'); - -function getAvatar(id, username) { - if(!config) return ''; - if(id===0) { - return config.AnonymousAvatar; - }else{ - return config.Avatar.replace(/\$\{username\}/g, username); - } -} +function createThread(post) { + var thread = new Thread(); + var object = thread.object; -function getTimeString(time) { - var m = moment(time).locale(mw.config.get('wgUserLanguage')); - var diff = Date.now() - time; - if(0 < diff && diff < 24*3600*1000) { - return m.fromNow(); - }else{ - return m.format('LL, HH:mm:ss'); - } -} + thread.init(post); -function wrapText(text) { - var span = $(''); - span.text(text); - return span.wrapAll('
').parent().html(); -} - -function wrapPageLink(page, name) { - var link = $(''); - link.attr('href', mw.util.getUrl(page)); - link.text(name); - return link.wrapAll('
').parent().html(); -} + // Enhance the username by adding page title + var pageLink = wrapPageLink(post.title, post.title); + object.find('.comment-user').html( + mw.msg('flowthread-ui-user-post-on-page', object.find('.comment-user').html(), pageLink)); -function Thread(post) { - var self = this; - var object = $(template); + thread.addButton('like', mw.msg('flowthread-ui-like') + '(' + post.like + ')', function() { + }); - this.post = post; - this.object = object; - $.data(object[0], 'thread', this); + thread.addButton('report', mw.msg('flowthread-ui-report') + '(' + post.report + ')', function() { + }); - var userlink; - if (post.userid !== 0) { - userlink = wrapPageLink('User:' + post.username, post.username); - } else { - userlink = wrapText(post.username); + if(!deleted){ + thread.addButton('delete', mw.msg('flowthread-ui-delete'), function() { + thread.delete(); + }); + }else{ + thread.addButton('recover', mw.msg('flowthread-ui-recover'), function() { + thread.recover(); + }); + thread.addButton('delete', mw.msg('flowthread-ui-erase'), function() { + thread.erase(); + }); } - var pageLink = wrapPageLink(post.title, post.title); - object.find('.comment-user').html(mw.msg('flowthread-ui-user-post-on-page', userlink, pageLink)); - - object.find('.comment-avatar img').attr('src', getAvatar(post.userid, post.username)); - - object.find('.comment-text').html(post.text); - object.find('.comment-time').text(getTimeString(post.timestamp*1000)); - - object.find('.comment-delete').click(function() { - self.delete(post.id); - }); - object.find('.comment-recover').click(function() { - self.recover(post.id); - }); - object.find('.comment-delete').click(function() { - self.erase(post.id); - }); object.find('.comment-avatar').click(function() { object.toggleClass('comment-selected'); onSelect(); - }) + }); - object.find('.comment-like span').text('(' + post.like + ')'); - object.find('.comment-report span').text('(' + post.report + ')'); - + return thread; } -Thread.prototype.delete = function() { - var api = new mw.Api(); - api.get({ - action: 'flowthread', - type: 'delete', - postid: this.post.id - }); - this.object.remove(); -}; - Thread.prototype.recover = function() { var api = new mw.Api(); api.get({ @@ -174,7 +104,7 @@ function loadComments() { var data = mw.config.get('commentjson'); $('.comment-container').html(''); data.forEach(function(item) { - $('.comment-container').append(new Thread(item).object); + $('.comment-container').append(createThread(item).object); }); }