From 6094883938ffb7a5dbc877e115edd9fa9f250e30 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Thu, 22 Oct 2026 15:41:11 +0200 Subject: [PATCH 1/2] Show the selected reaction icon on the reaction button Closes #6655 --- com.woltlab.wcf/templates/reactionButton.tpl | 6 +++- .../Core/Component/Reaction/Button.ts | 29 +++++++++++-------- .../Core/Component/Reaction/Button.js | 28 ++++++++++-------- .../system/reaction/ReactionData.class.php | 15 ++++++++++ .../install/files/style/ui/reactions.scss | 7 +++++ 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/com.woltlab.wcf/templates/reactionButton.tpl b/com.woltlab.wcf/templates/reactionButton.tpl index 9b62c182c73..b3e89234170 100644 --- a/com.woltlab.wcf/templates/reactionButton.tpl +++ b/com.woltlab.wcf/templates/reactionButton.tpl @@ -7,5 +7,9 @@ data-reaction-object-type="{$reactionData->objectType}" data-object-id="{$reactionData->objectID}" > - {icon name='face-smile'} + {if $reactionData->getReactionType()} + {unsafe:$reactionData->getReactionType()->renderIcon()} + {else} + {icon name='face-smile'} + {/if} diff --git a/ts/WoltLabSuite/Core/Component/Reaction/Button.ts b/ts/WoltLabSuite/Core/Component/Reaction/Button.ts index 9eaf4f9ab8d..366fcc340a1 100644 --- a/ts/WoltLabSuite/Core/Component/Reaction/Button.ts +++ b/ts/WoltLabSuite/Core/Component/Reaction/Button.ts @@ -267,6 +267,21 @@ function updateReactionSummary( component?.setData(reactions, selectedReaction); } +function updateButton(button: HTMLButtonElement, reactionTypeId: number): void { + button.dataset.reactionTypeId = reactionTypeId.toString(); + button.classList.toggle("active", reactionTypeId !== 0); + button.setAttribute("aria-pressed", reactionTypeId !== 0 ? "true" : "false"); + + const reactionType = window.REACTION_TYPES[reactionTypeId]; + if (reactionType !== undefined) { + button.innerHTML = reactionType.renderedIcon; + } else { + const icon = document.createElement("fa-icon"); + icon.setIcon("face-smile"); + button.replaceChildren(icon); + } +} + function setupToggleButton(): void { wheneverFirstSeen("[data-reaction-object-type]", (button: HTMLButtonElement) => { button.addEventListener( @@ -284,17 +299,12 @@ async function toggleButton(button: HTMLButtonElement): Promise { if (button.classList.contains("active")) { reactions = (await revertReaction(objectType, objectId)).reactions; - button.dataset.reactionTypeId = "0"; - button.classList.remove("active"); - button.setAttribute("aria-pressed", "false"); } else { reactionTypeId = availableReactions[0].reactionTypeID; reactions = (await setReaction(objectType, objectId, reactionTypeId)).reactions; - button.dataset.reactionTypeId = reactionTypeId.toString(); - button.classList.add("active"); - button.setAttribute("aria-pressed", "true"); } + updateButton(button, reactionTypeId); updateReactionSummary(objectType, objectId, reactions, reactionTypeId); } @@ -334,17 +344,12 @@ function setupPopoverButton(): void { if (result.reactionTypeId == oldReactionTypeId) { reactions = (await revertReaction(objectType, objectId)).reactions; - button.dataset.reactionTypeId = "0"; - button.classList.remove("active"); - button.setAttribute("aria-pressed", "false"); } else { reactionTypeId = result.reactionTypeId; reactions = (await setReaction(objectType, objectId, reactionTypeId)).reactions; - button.dataset.reactionTypeId = reactionTypeId.toString(); - button.classList.add("active"); - button.setAttribute("aria-pressed", "true"); } + updateButton(button, reactionTypeId); updateReactionSummary(objectType, objectId, reactions, reactionTypeId); }); }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js index b23668dbb83..efba3c3e483 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js @@ -195,6 +195,20 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/Reactions/RevertRe const component = document.querySelector(`woltlab-core-reaction-summary[object-type="${objectType}"][object-id="${objectId}"]`); component?.setData(reactions, selectedReaction); } + function updateButton(button, reactionTypeId) { + button.dataset.reactionTypeId = reactionTypeId.toString(); + button.classList.toggle("active", reactionTypeId !== 0); + button.setAttribute("aria-pressed", reactionTypeId !== 0 ? "true" : "false"); + const reactionType = window.REACTION_TYPES[reactionTypeId]; + if (reactionType !== undefined) { + button.innerHTML = reactionType.renderedIcon; + } + else { + const icon = document.createElement("fa-icon"); + icon.setIcon("face-smile"); + button.replaceChildren(icon); + } + } function setupToggleButton() { (0, Selector_1.wheneverFirstSeen)("[data-reaction-object-type]", (button) => { button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => toggleButton(button))); @@ -207,17 +221,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/Reactions/RevertRe let reactions; if (button.classList.contains("active")) { reactions = (await (0, RevertReaction_1.revertReaction)(objectType, objectId)).reactions; - button.dataset.reactionTypeId = "0"; - button.classList.remove("active"); - button.setAttribute("aria-pressed", "false"); } else { reactionTypeId = availableReactions[0].reactionTypeID; reactions = (await (0, SetReaction_1.setReaction)(objectType, objectId, reactionTypeId)).reactions; - button.dataset.reactionTypeId = reactionTypeId.toString(); - button.classList.add("active"); - button.setAttribute("aria-pressed", "true"); } + updateButton(button, reactionTypeId); updateReactionSummary(objectType, objectId, reactions, reactionTypeId); } function setupPopoverButton() { @@ -249,17 +258,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/Reactions/RevertRe let reactions; if (result.reactionTypeId == oldReactionTypeId) { reactions = (await (0, RevertReaction_1.revertReaction)(objectType, objectId)).reactions; - button.dataset.reactionTypeId = "0"; - button.classList.remove("active"); - button.setAttribute("aria-pressed", "false"); } else { reactionTypeId = result.reactionTypeId; reactions = (await (0, SetReaction_1.setReaction)(objectType, objectId, reactionTypeId)).reactions; - button.dataset.reactionTypeId = reactionTypeId.toString(); - button.classList.add("active"); - button.setAttribute("aria-pressed", "true"); } + updateButton(button, reactionTypeId); updateReactionSummary(objectType, objectId, reactions, reactionTypeId); }); }); diff --git a/wcfsetup/install/files/lib/system/reaction/ReactionData.class.php b/wcfsetup/install/files/lib/system/reaction/ReactionData.class.php index 9acb2e6be1a..f44757042f9 100644 --- a/wcfsetup/install/files/lib/system/reaction/ReactionData.class.php +++ b/wcfsetup/install/files/lib/system/reaction/ReactionData.class.php @@ -2,6 +2,9 @@ namespace wcf\system\reaction; +use wcf\data\reaction\type\ReactionType; +use wcf\data\reaction\type\ReactionTypeCache; + /** * Data transfer object for an object that has reactions. * @@ -19,4 +22,16 @@ public function __construct( public readonly ?string $cachedReactions = null, public readonly string $reactionsJson = '[]', ) {} + + /** + * Returns the reaction type the active user has reacted with or `null` if there is none. + */ + public function getReactionType(): ?ReactionType + { + if ($this->reactionTypeID === 0) { + return null; + } + + return ReactionTypeCache::getInstance()->getReactionTypeByID($this->reactionTypeID); + } } diff --git a/wcfsetup/install/files/style/ui/reactions.scss b/wcfsetup/install/files/style/ui/reactions.scss index 67f190068f5..31c712e12dd 100644 --- a/wcfsetup/install/files/style/ui/reactions.scss +++ b/wcfsetup/install/files/style/ui/reactions.scss @@ -38,6 +38,13 @@ html[data-color-scheme="dark"] .reactionPopover { height: 20px; } +.reactionButton .reactionType { + width: 16px; + height: 16px; + // Necessary because the default icon has a size of 20x16. + margin: 0 2px; +} + .reactionPopoverContent .reactionType, .reactionTypeListImage .reactionType { width: 24px; From 639752c9f2d57ff5667aa5342764c3cffc923212 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Thu, 22 Oct 2026 15:51:03 +0200 Subject: [PATCH 2/2] Animate the reaction button icon on change --- .../Core/Component/Reaction/Button.ts | 3 +++ .../Core/Component/Reaction/Button.js | 2 ++ .../install/files/style/ui/reactions.scss | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Reaction/Button.ts b/ts/WoltLabSuite/Core/Component/Reaction/Button.ts index 366fcc340a1..f27c660967a 100644 --- a/ts/WoltLabSuite/Core/Component/Reaction/Button.ts +++ b/ts/WoltLabSuite/Core/Component/Reaction/Button.ts @@ -272,6 +272,9 @@ function updateButton(button: HTMLButtonElement, reactionTypeId: number): void { button.classList.toggle("active", reactionTypeId !== 0); button.setAttribute("aria-pressed", reactionTypeId !== 0 ? "true" : "false"); + // Animates the new icon, the server-rendered initial icon is not affected. + button.classList.add("reactionButton--animated"); + const reactionType = window.REACTION_TYPES[reactionTypeId]; if (reactionType !== undefined) { button.innerHTML = reactionType.renderedIcon; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js index efba3c3e483..9a6bb114993 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/Button.js @@ -199,6 +199,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/Reactions/RevertRe button.dataset.reactionTypeId = reactionTypeId.toString(); button.classList.toggle("active", reactionTypeId !== 0); button.setAttribute("aria-pressed", reactionTypeId !== 0 ? "true" : "false"); + // Animates the new icon, the server-rendered initial icon is not affected. + button.classList.add("reactionButton--animated"); const reactionType = window.REACTION_TYPES[reactionTypeId]; if (reactionType !== undefined) { button.innerHTML = reactionType.renderedIcon; diff --git a/wcfsetup/install/files/style/ui/reactions.scss b/wcfsetup/install/files/style/ui/reactions.scss index 31c712e12dd..2bb4792fc4d 100644 --- a/wcfsetup/install/files/style/ui/reactions.scss +++ b/wcfsetup/install/files/style/ui/reactions.scss @@ -45,6 +45,30 @@ html[data-color-scheme="dark"] .reactionPopover { margin: 0 2px; } +@keyframes reactionButtonIcon { + 0% { + opacity: 0; + transform: scale(0.6); + } + 60% { + opacity: 1; + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + +.reactionButton--animated > * { + animation: reactionButtonIcon 0.36s ease-out; +} + +@media (prefers-reduced-motion: reduce) { + .reactionButton--animated > * { + animation: none; + } +} + .reactionPopoverContent .reactionType, .reactionTypeListImage .reactionType { width: 24px;