From f3baec657602b74e3d29b2264dbf786b235b1330 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Joshua=20R=C3=BCsweg?= Date: Sun, 28 Oct 2018 14:57:32 +0100 Subject: [PATCH] Add reactionCount buttons for articles See #2508 --- com.woltlab.wcf/templates/article.tpl | 6 +- .../templates/articleListItems.tpl | 8 +- com.woltlab.wcf/templates/boxArticleList.tpl | 4 +- .../WoltLabSuite/Core/Ui/Reaction/Handler.js | 18 +- .../Core/Ui/Reaction/ReputationButtons.js | 157 ++++++++++++++++++ .../article/ViewableArticleList.class.php | 5 +- .../lib/data/like/object/LikeObject.class.php | 31 ++++ .../data/reaction/ReactionAction.class.php | 1 + .../system/reaction/ReactionHandler.class.php | 13 +- .../install/files/style/ui/reactions.scss | 23 +++ 10 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/ReputationButtons.js diff --git a/com.woltlab.wcf/templates/article.tpl b/com.woltlab.wcf/templates/article.tpl index 098fdbda05..b336d01d0f 100644 --- a/com.woltlab.wcf/templates/article.tpl +++ b/com.woltlab.wcf/templates/article.tpl @@ -54,7 +54,11 @@ {if $article->isDeleted}
  • {lang}wcf.message.status.deleted{/lang}
  • {/if} -
  • + {if $articleLikeData|isset && $articleLikeData[$article->articleID]|isset && !$articleLikeData[$article->articleID]->getReactions()|empty} +
  • {if $articleLikeData[$article->articleID]->getReputation() > 0}+{elseif $articleLikeData[$article->articleID]->getReputation() < 0}−{else}±{/if}{#$articleLikeData[$article->articleID]->getReputation()}
  • + {else} +
  • + {/if} {event name='contentHeaderMetaData'} diff --git a/com.woltlab.wcf/templates/articleListItems.tpl b/com.woltlab.wcf/templates/articleListItems.tpl index 1908868287..e98ef33717 100644 --- a/com.woltlab.wcf/templates/articleListItems.tpl +++ b/com.woltlab.wcf/templates/articleListItems.tpl @@ -35,11 +35,9 @@ {/if} {if MODULE_LIKE && $__wcf->getSession()->getPermission('user.like.canViewLike')} -
  • - {if $article->likes || $article->dislikes || $article->neutralReactions} - {if $article->cumulativeLikes > 0}+{elseif $article->cumulativeLikes == 0}±{/if}{#$article->cumulativeLikes} - {/if} -
  • + {if $article->likes || $article->dislikes || $article->neutralReactions} +
  • {if $article->cumulativeLikes > 0}+{elseif $article->cumulativeLikes == 0}±{/if}{#$article->cumulativeLikes}
  • + {/if} {/if} {if ARTICLE_ENABLE_VISIT_TRACKING && $article->isNew()}
  • {lang}wcf.message.new{/lang}
  • {/if} diff --git a/com.woltlab.wcf/templates/boxArticleList.tpl b/com.woltlab.wcf/templates/boxArticleList.tpl index 881d56121f..ac166e7b6d 100644 --- a/com.woltlab.wcf/templates/boxArticleList.tpl +++ b/com.woltlab.wcf/templates/boxArticleList.tpl @@ -16,9 +16,7 @@ {$boxArticle->getDiscussionProvider()->getDiscussionCountPhrase()} {elseif $boxSortField == 'cumulativeLikes'} {if MODULE_LIKE && $__wcf->getSession()->getPermission('user.like.canViewLike') && ($boxArticle->likes || $boxArticle->dislikes)} - - {if $boxArticle->cumulativeLikes > 0}+{elseif $boxArticle->cumulativeLikes == 0}±{/if}{#$boxArticle->cumulativeLikes} - + {if $boxArticle->cumulativeLikes > 0}+{elseif $boxArticle->cumulativeLikes == 0}±{/if}{#$boxArticle->cumulativeLikes} {/if} {/if} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js index 67130a5f51..721e746a65 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/Handler.js @@ -9,16 +9,16 @@ */ define( [ - 'Ajax', 'Core', 'Dictionary', 'Language', - 'ObjectMap', 'StringUtil', 'Dom/ChangeListener', 'Dom/Util', - 'Ui/Dialog', 'WoltLabSuite/Core/Ui/User/List', 'User', 'WoltLabSuite/Core/Ui/Reaction/CountButtons', - 'Ui/Alignment', 'Ui/CloseOverlay', 'Ui/Screen' + 'Ajax', 'Core', 'Dictionary', 'Language', + 'ObjectMap', 'StringUtil', 'Dom/ChangeListener', 'Dom/Util', + 'Ui/Dialog', 'WoltLabSuite/Core/Ui/User/List', 'User', 'WoltLabSuite/Core/Ui/Reaction/CountButtons', + 'Ui/Alignment', 'Ui/CloseOverlay', 'Ui/Screen', 'WoltLabSuite/Core/Ui/Reaction/ReputationButtons', ], function( - Ajax, Core, Dictionary, Language, - ObjectMap, StringUtil, DomChangeListener, DomUtil, - UiDialog, UiUserList, User, CountButtons, - UiAlignment, UiCloseOverlay, UiScreen + Ajax, Core, Dictionary, Language, + ObjectMap, StringUtil, DomChangeListener, DomUtil, + UiDialog, UiUserList, User, CountButtons, + UiAlignment, UiCloseOverlay, UiScreen, ReputationButtons ) { "use strict"; @@ -64,6 +64,7 @@ define( this.initReactButtons(options, objectType); this.countButtons = new CountButtons(this._objectType, this._options); + this.reputationButtons = new ReputationButtons(this._objectType); DomChangeListener.add('WoltLabSuite/Core/Ui/Reaction/Handler-' + objectType, this.initReactButtons.bind(this)); UiCloseOverlay.add('WoltLabSuite/Core/Ui/Reaction/Handler', this._closePopover.bind(this)); @@ -363,6 +364,7 @@ define( _ajaxSuccess: function(data) { this.countButtons.updateCountButtons(data.returnValues.objectID, data.returnValues.reactions); + this.reputationButtons.setReputationCount(data.returnValues.objectID, data.returnValues.reputationCount); // update react button status this._updateReactButton(data.returnValues.objectID, data.returnValues.reactionTypeID); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/ReputationButtons.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/ReputationButtons.js new file mode 100644 index 0000000000..6608cac0c8 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/ReputationButtons.js @@ -0,0 +1,157 @@ +/** + * Handles the reputation count buttons. + * + * @author Joshua Ruesweg + * @copyright 2001-2018 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Ui/Reaction/Handler + * @since 3.2 + */ +define( + ['Ajax', 'Dictionary', 'Dom/ChangeListener', 'Ui/Dialog'], + function(Ajax, Dictionary, DomChangeListener, UiDialog) + { + "use strict"; + + /** + * @constructor + */ + function ReputationButtons(objectType, options) { this.init(objectType, options); } + ReputationButtons.prototype = { + /** + * Initializes the reputation buttons. + * + * @param {string} objectType object type + */ + init: function(objectType) { + if (objectType === '') { + throw new Error("[WoltLabSuite/Core/Ui/Reaction/ReputationButtons] Expected a non-empty string for objectType 'containerSelector'."); + } + + this._containers = new Dictionary(); + this._objectType = objectType; + + this.initContainers(); + + DomChangeListener.add('WoltLabSuite/Core/Ui/Reaction/ReputationButtons-' + objectType, this.initContainers.bind(this)); + }, + + /** + * Initialises the containers. + */ + initContainers: function() { + var element, elements = elBySelAll(".reputationCounter[data-object-type='" + this._objectType + "']"), elementData, triggerChange = false, objectId; + for (var i = 0, length = elements.length; i < length; i++) { + element = elements[i]; + objectId = ~~elData(element, 'object-id'); + if (this._containers.has(objectId)) { + continue; + } + + elementData = { + objectId: ~~elData(element, 'object-id'), + element: element + }; + + this._containers.set(objectId, elementData); + + this._initReputationCountButton(elementData.element, elementData.objectId); + + triggerChange = true; + } + + if (triggerChange) { + DomChangeListener.trigger(); + } + }, + + /** + * Sets the count of a specific reputation button. + * + * @param {int} objectId + * @param {int} count + */ + setReputationCount: function(objectId, count) { + if (this._containers.has(objectId)) { + this._containers.get(objectId).element.classList.remove("neutral", "negative", "positive"); + + if (count > 0) { + this._containers.get(objectId).element.innerHTML = "+" + count; + this._containers.get(objectId).element.classList.add("positive"); + } + else if (count < 0) { + this._containers.get(objectId).element.innerHTML = count; + this._containers.get(objectId).element.classList.add("negative"); + } + else if (count === 0) { + this._containers.get(objectId).element.innerHTML = "±" + count; + this._containers.get(objectId).element.classList.add("neutral"); + } + else { + this._containers.get(objectId).element.innerHTML = ""; + } + } + }, + + /** + * Initialized a specific reaction count button for an object. + * + * @param {element} element + * @param {int} objectId + */ + _initReputationCountButton: function(element, objectId) { + element.addEventListener(WCF_CLICK_EVENT, this._showReactionOverlay.bind(this, objectId)); + }, + + /** + * Shows the reaction overly for a specific object. + * + * @param {int} objectId + */ + _showReactionOverlay: function(objectId) { + this._currentObjectId = objectId; + this._showOverlay(); + }, + + /** + * Shows the overlay for the reactions. + */ + _showOverlay: function() { + Ajax.api(this, { + parameters: { + data: { + containerID: this._objectType + '-' + this._currentObjectId, + objectID: this._currentObjectId, + objectType: this._objectType + } + } + }); + }, + + _ajaxSuccess: function(data) { + UiDialog.open(this, data.returnValues.template); + UiDialog.setTitle('userReactionOverlay-' + this._objectType, data.returnValues.title); + }, + + _ajaxSetup: function() { + return { + data: { + actionName: 'getReactionDetails', + className: '\\wcf\\data\\reaction\\ReactionAction' + } + }; + }, + + _dialogSetup: function() { + return { + id: 'userReactionOverlay-' + this._objectType, + options: { + title: "" + }, + source: null + }; + } + }; + + return ReputationButtons; + }); diff --git a/wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php b/wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php index ab6cc05ca5..e9721fb2c3 100644 --- a/wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php +++ b/wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php @@ -4,6 +4,7 @@ use wcf\data\article\content\ViewableArticleContentList; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\label\object\ArticleLabelObjectHandler; use wcf\system\like\LikeHandler; +use wcf\system\reaction\ReactionHandler; use wcf\system\visitTracker\VisitTracker; use wcf\system\WCF; @@ -48,8 +49,8 @@ class ViewableArticleList extends ArticleList { // get like status if (!empty($this->sqlSelects)) $this->sqlSelects .= ','; - $this->sqlSelects .= "like_object.likes, like_object.dislikes"; - $this->sqlJoins .= " LEFT JOIN wcf".WCF_N."_like_object like_object ON (like_object.objectTypeID = ".LikeHandler::getInstance()->getObjectType('com.woltlab.wcf.likeableArticle')->objectTypeID." AND like_object.objectID = article.articleID)"; + $this->sqlSelects .= "like_object.likes, like_object.dislikes, like_object.neutralReactions"; + $this->sqlJoins .= " LEFT JOIN wcf".WCF_N."_like_object like_object ON (like_object.objectTypeID = ".ReactionHandler::getInstance()->getObjectType('com.woltlab.wcf.likeableArticle')->objectTypeID." AND like_object.objectID = article.articleID)"; } /** diff --git a/wcfsetup/install/files/lib/data/like/object/LikeObject.class.php b/wcfsetup/install/files/lib/data/like/object/LikeObject.class.php index 41a06a7cf4..c9f46d9607 100644 --- a/wcfsetup/install/files/lib/data/like/object/LikeObject.class.php +++ b/wcfsetup/install/files/lib/data/like/object/LikeObject.class.php @@ -4,6 +4,7 @@ use wcf\data\object\type\ObjectTypeCache; use wcf\data\reaction\type\ReactionTypeCache; use wcf\data\user\User; use wcf\data\DatabaseObject; +use wcf\system\reaction\ReactionHandler; use wcf\system\WCF; use wcf\util\JSON; @@ -50,6 +51,12 @@ class LikeObject extends DatabaseObject { */ protected $reactions = []; + /** + * The reputation for the current object. + * @var integer + */ + protected $reputation = null; + /** * @inheritDoc */ @@ -144,6 +151,30 @@ class LikeObject extends DatabaseObject { return $this->reactions; } + /** + * Returns the reputation of the current like object. Returns null, if + * there are no reactions on the object. + * + * @return integer|null + * @since 3.2 + */ + public function getReputation() { + if ($this->reputation === null && !empty($this->getReactions())) { + $this->reputation = 0; + foreach ($this->getReactions() as $reactionTypeID => $data) { + $reactionType = ReactionHandler::getInstance()->getReactionTypeByID($reactionTypeID); + if ($reactionType->isPositive()) { + $this->reputation += $data['reactionCount']; + } + else if ($reactionType->isNegative()) { + $this->reputation -= $data['reactionCount']; + } + } + } + + return $this->reputation; + } + /** * Sets the liked object. * diff --git a/wcfsetup/install/files/lib/data/reaction/ReactionAction.class.php b/wcfsetup/install/files/lib/data/reaction/ReactionAction.class.php index ad1e37fb47..fc99aa0737 100644 --- a/wcfsetup/install/files/lib/data/reaction/ReactionAction.class.php +++ b/wcfsetup/install/files/lib/data/reaction/ReactionAction.class.php @@ -166,6 +166,7 @@ class ReactionAction extends AbstractDatabaseObjectAction { 'objectType' => $this->parameters['data']['objectType'], 'reactionTypeID' => $reactionData['reactionTypeID'], 'containerID' => $this->parameters['data']['containerID'], + 'reputationCount' => $reactionData['cumulativeLikes'] ]; } diff --git a/wcfsetup/install/files/lib/system/reaction/ReactionHandler.class.php b/wcfsetup/install/files/lib/system/reaction/ReactionHandler.class.php index cc3a0490c9..a1e0fb6530 100644 --- a/wcfsetup/install/files/lib/system/reaction/ReactionHandler.class.php +++ b/wcfsetup/install/files/lib/system/reaction/ReactionHandler.class.php @@ -376,7 +376,8 @@ class ReactionHandler extends SingletonFactory { return [ 'cachedReactions' => $likeObjectData['cachedReactions'], 'reactionTypeID' => $reactionTypeID, - 'likeObject' => $likeObjectData['likeObject'] + 'likeObject' => $likeObjectData['likeObject'], + 'cumulativeLikes' => $likeObjectData['cumulativeLikes'] ]; } catch (DatabaseQueryException $e) { @@ -584,7 +585,8 @@ class ReactionHandler extends SingletonFactory { return [ 'cachedReactions' => $likeObjectData['cachedReactions'], 'reactionTypeID' => null, - 'likeObject' => $likeObjectData['likeObject'] + 'likeObject' => $likeObjectData['likeObject'], + 'cumulativeLikes' => $likeObjectData['cumulativeLikes'] ]; } catch (DatabaseQueryException $e) { @@ -594,7 +596,8 @@ class ReactionHandler extends SingletonFactory { return [ 'cachedReactions' => [], 'reactionTypeID' => null, - 'likeObject' => [] + 'likeObject' => [], + 'cumulativeLikes' => null ]; } @@ -635,6 +638,10 @@ class ReactionHandler extends SingletonFactory { } } + if (!$likes && !$dislikes) { + $cumulativeLikes = null; + } + // build update date $updateData = [ 'likes' => $likes, diff --git a/wcfsetup/install/files/style/ui/reactions.scss b/wcfsetup/install/files/style/ui/reactions.scss index f56b5ec8a8..488afb2162 100644 --- a/wcfsetup/install/files/style/ui/reactions.scss +++ b/wcfsetup/install/files/style/ui/reactions.scss @@ -122,6 +122,29 @@ li.reactCountButton:hover { color: $wcfContentText; } +.reputationCounter { + color: $wcfContentDimmedText; + cursor: pointer; + + &.positive { + color: #060 !important; + } + + &.negative { + color: #900 !important; + } + + &::before { + font-family: FontAwesome; + content: $fa-var-smile-o + "\00A0"; + display: inline-block; + } + + &:empty { + display: none; + } +} + .reactionSummaryListTiny { display: inline; -- 2.20.1