From f5ff72dab13ac1c158458611b0e5a3de6e53e8b3 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sat, 2 May 2026 13:26:49 +0200 Subject: [PATCH 1/3] Redesign reaction summary details dialog --- .../reactionSummaryDetailsListItems.tpl | 52 ++++++++ ts/WoltLabSuite/Core/BootstrapFrontend.ts | 2 +- .../Core/Component/Reaction/SummaryDetails.ts | 37 ++++++ .../Core/Ui/Reaction/SummaryDetails.ts | 1 + .../js/WoltLabSuite/Core/BootstrapFrontend.js | 2 +- .../Core/Component/Reaction/SummaryDetails.js | 25 ++++ .../Core/Ui/Reaction/SummaryDetails.js | 1 + ...ummaryDetailsListViewInitialized.class.php | 19 +++ .../ReactionSummaryDetailsListView.class.php | 115 ++++++++++++++++++ .../files/style/ui/simpleUserList.scss | 69 +++++++++++ 10 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl create mode 100644 ts/WoltLabSuite/Core/Component/Reaction/SummaryDetails.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetails.js create mode 100644 wcfsetup/install/files/lib/event/listView/user/ReactionSummaryDetailsListViewInitialized.class.php create mode 100644 wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php create mode 100644 wcfsetup/install/files/style/ui/simpleUserList.scss diff --git a/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl b/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl new file mode 100644 index 00000000000..06db73d09f4 --- /dev/null +++ b/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl @@ -0,0 +1,52 @@ +{foreach from=$view->getItems() item='reaction'} + {assign var='user' value=$reaction->getUserProfile()} +
+
+ {unsafe:$user->getAvatar()->getImageTag(96)} +
+ +
+
+

+ {unsafe:$user->getFormattedUsername()} +

+ {if MODULE_USER_RANK && $user->getUserTitle()} + {$user->getUserTitle()} + {/if} +
+
+ {time time=$reaction->time} +
+
+ +
+ {unsafe:$reaction->render()} + +
+ {if $__wcf->user->userID && $user->userID != $__wcf->user->userID} + {if !$__wcf->getUserProfileHandler()->isIgnoredByUser($user->userID)} + {if $__wcf->getUserProfileHandler()->isFollowing($user->userID)} + + {else} + + {/if} + {/if} + {/if} + + {unsafe:$view->renderInteractionContextMenuButton($reaction)} +
+
+
+{/foreach} diff --git a/ts/WoltLabSuite/Core/BootstrapFrontend.ts b/ts/WoltLabSuite/Core/BootstrapFrontend.ts index 4c1ce97baa3..c6e189dda65 100644 --- a/ts/WoltLabSuite/Core/BootstrapFrontend.ts +++ b/ts/WoltLabSuite/Core/BootstrapFrontend.ts @@ -148,7 +148,7 @@ export function setup(options: BootstrapOptions): void { } whenFirstSeen("woltlab-core-reaction-summary", () => { - void import("./Ui/Reaction/SummaryDetails").then(({ setup }) => setup()); + void import("./Component/Reaction/SummaryDetails").then(({ setup }) => setup()); }); whenFirstSeen("woltlab-core-comment", () => { void import("./Component/Comment/woltlab-core-comment"); diff --git a/ts/WoltLabSuite/Core/Component/Reaction/SummaryDetails.ts b/ts/WoltLabSuite/Core/Component/Reaction/SummaryDetails.ts new file mode 100644 index 00000000000..fe06e64c90d --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Reaction/SummaryDetails.ts @@ -0,0 +1,37 @@ +/** + * Handles the reaction summary details dialog. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { getPhrase } from "WoltLabSuite/Core/Language"; +import { dialogFactory } from "../../Component/Dialog"; +import { wheneverFirstSeen } from "../../Helper/Selector"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; + +export function setup(): void { + wheneverFirstSeen("woltlab-core-reaction-summary", (element: WoltlabCoreReactionSummaryElement) => { + element.addEventListener( + "showDetails", + promiseMutex(() => { + return dialogFactory() + .usingListView() + .fromPreset( + getPhrase("wcf.reactions.summary.title"), + "wcf\\system\\listView\\user\\ReactionSummaryDetailsListView", + 1, + "", + "ASC", + undefined, + new Map([ + ["objectID", element.objectId.toString()], + ["objectType", element.objectType], + ]), + ); + }), + ); + }); +} diff --git a/ts/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.ts b/ts/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.ts index 6f0fbf9e21a..e1406d81ab6 100644 --- a/ts/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.ts +++ b/ts/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.ts @@ -5,6 +5,7 @@ * @copyright 2001-2022 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.0 + * @deprecated 6.3 */ import { dboAction } from "../../Ajax"; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js index 570938fabbb..bf847739b1e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js @@ -103,7 +103,7 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui } } (0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => { - void new Promise((resolve_6, reject_6) => { require(["./Ui/Reaction/SummaryDetails"], resolve_6, reject_6); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_6, reject_6) => { require(["./Component/Reaction/SummaryDetails"], resolve_6, reject_6); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment", () => { void new Promise((resolve_7, reject_7) => { require(["./Component/Comment/woltlab-core-comment"], resolve_7, reject_7); }).then(tslib_1.__importStar); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetails.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetails.js new file mode 100644 index 00000000000..57fcf48190f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetails.js @@ -0,0 +1,25 @@ +/** + * Handles the reaction summary details dialog. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ +define(["require", "exports", "WoltLabSuite/Core/Language", "../../Component/Dialog", "../../Helper/Selector", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, Language_1, Dialog_1, Selector_1, PromiseMutex_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setup = setup; + function setup() { + (0, Selector_1.wheneverFirstSeen)("woltlab-core-reaction-summary", (element) => { + element.addEventListener("showDetails", (0, PromiseMutex_1.promiseMutex)(() => { + return (0, Dialog_1.dialogFactory)() + .usingListView() + .fromPreset((0, Language_1.getPhrase)("wcf.reactions.summary.title"), "wcf\\system\\listView\\user\\ReactionSummaryDetailsListView", 1, "", "ASC", undefined, new Map([ + ["objectID", element.objectId.toString()], + ["objectType", element.objectType], + ])); + })); + }); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.js index 4ba70c4e5c1..5587f339e47 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Reaction/SummaryDetails.js @@ -5,6 +5,7 @@ * @copyright 2001-2022 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.0 + * @deprecated 6.3 */ define(["require", "exports", "../../Ajax", "../../Component/Dialog", "../../Helper/Selector"], function (require, exports, Ajax_1, Dialog_1, Selector_1) { "use strict"; diff --git a/wcfsetup/install/files/lib/event/listView/user/ReactionSummaryDetailsListViewInitialized.class.php b/wcfsetup/install/files/lib/event/listView/user/ReactionSummaryDetailsListViewInitialized.class.php new file mode 100644 index 00000000000..a387b289e0b --- /dev/null +++ b/wcfsetup/install/files/lib/event/listView/user/ReactionSummaryDetailsListViewInitialized.class.php @@ -0,0 +1,19 @@ + + * @since 6.3 + */ +final class ReactionSummaryDetailsListViewInitialized implements IPsr14Event +{ + public function __construct(public readonly ReactionSummaryDetailsListView $listView) {} +} diff --git a/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php new file mode 100644 index 00000000000..449afb85ce7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php @@ -0,0 +1,115 @@ + + * @since 6.3 + * + * @extends AbstractListView + */ +class ReactionSummaryDetailsListView extends AbstractListView +{ + public function __construct( + public readonly string $objectType, + public readonly int $objectID + ) { + $this->addAvailableSortFields([ + new ListViewSortField( + 'username', + 'wcf.user.username', + '(SELECT username FROM wcf1_user WHERE userID = like_table.userID)' + ), + ]); + + $this->setAllowSorting(false); + $this->setInteractionProvider(new UserProfileInteractions()); + $this->setDefaultSortField('username'); + $this->setItemsPerPage(100); + $this->setCssClassName('simpleUserList'); + $this->setContainerCssClassName('simpleUserList__container'); + } + + #[\Override] + protected function createObjectList(): ViewableLikeList + { + $likeList = new ViewableLikeList(); + $likeList->getConditionBuilder()->add('objectTypeID = ?', [ + ReactionHandler::getInstance()->getObjectType($this->objectType)->objectTypeID + ]); + $likeList->getConditionBuilder()->add('objectID = ?', [$this->objectID]); + + return $likeList; + } + + #[\Override] + public function isAccessible(): bool + { + if (!WCF::getSession()->hasPermission('user.like.canViewLike')) { + return false; + } + + $objectType = ReactionHandler::getInstance()->getObjectType($this->objectType); + if ($objectType === null) { + return false; + } + + $objectTypeProvider = $objectType->getProcessor(); + \assert($objectTypeProvider instanceof ILikeObjectTypeProvider); + + $likeableObject = $objectTypeProvider->getObjectByID($this->objectID); + \assert($likeableObject instanceof ILikeObject); + $likeableObject->setObjectType($objectType); + + if ($objectTypeProvider instanceof IRestrictedLikeObjectTypeProvider) { + if (!$objectTypeProvider->canViewLikes($likeableObject)) { + return false; + } + } elseif (!$objectTypeProvider->checkPermissions($likeableObject)) { + return false; + } + + return true; + } + + #[\Override] + public function renderItems(): string + { + return WCF::getTPL()->render('wcf', 'reactionSummaryDetailsListItems', ['view' => $this]); + } + + #[\Override] + public function renderInteractionContextMenuButton(DatabaseObject $item): string + { + if (!$this->hasInteractions()) { + return ''; + } + + \assert($item instanceof ViewableLike); + + return $this->getInteractionContextMenuComponent()->renderButton($item->getUserProfile()); + } + + #[\Override] + protected function getInitializedEvent(): ReactionSummaryDetailsListViewInitialized + { + return new ReactionSummaryDetailsListViewInitialized($this); + } +} diff --git a/wcfsetup/install/files/style/ui/simpleUserList.scss b/wcfsetup/install/files/style/ui/simpleUserList.scss new file mode 100644 index 00000000000..7dfc949f8fc --- /dev/null +++ b/wcfsetup/install/files/style/ui/simpleUserList.scss @@ -0,0 +1,69 @@ +.simpleUserList { + display: flex; + flex-direction: column; +} + +.simpleUserList__item { + align-items: center; + display: flex; + column-gap: 10px; + padding: 10px; + border-radius: var(--wcfBorderRadius); +} + +@media (hover: hover) { + .simpleUserList__item:hover { + background-color: var(--wcfTabularBoxBackgroundActive); + } +} + +.simpleUserList__item__avatar img { + width: 48px; + height: 48px; +} + +.simpleUserList__item__content { + flex: 1 1 auto; +} + +.simpleUserList__item__title { + display: flex; + gap: 5px; + align-items: center; +} + +.simpleUserList__item__username { + @include wcfFontBold; +} + +.simpleUserList__item__link { + color: inherit; + + &:hover { + color: inherit; + } +} + +@media (hover: hover) { + .simpleUserList__item__link:hover { + text-decoration: underline; + text-underline-offset: 3px; + } +} + +.simpleUserList__item__description { + @include wcfFontSmall; + color: var(--wcfContentDimmedText); +} + +.simpleUserList__item__extra { + align-items: center; + column-gap: 10px; + display: flex; +} + +.simpleUserList__item__interactions { + align-items: center; + column-gap: 5px; + display: flex; +} From 22c8b6e79d0a77900d489bfb7a3ead8a4dd038db Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sat, 2 May 2026 19:18:39 +0200 Subject: [PATCH 2/3] Add filter button in reaction summary details --- .../reactionSummaryDetailsFilterButtons.tpl | 28 +++++ com.woltlab.wcf/templates/shared_listView.tpl | 102 ++++++++++-------- ts/WoltLabSuite/Core/Component/ListView.ts | 16 +++ .../Reaction/SummaryDetailsFilterButtons.ts | 35 ++++++ .../WoltLabSuite/Core/Component/ListView.js | 14 +++ .../Reaction/SummaryDetailsFilterButtons.js | 36 +++++++ ...PreloadPhrasesCollectingListener.class.php | 1 + .../listView/AbstractListView.class.php | 17 +++ .../ReactionSummaryDetailsListView.class.php | 61 ++++++++++- wcfsetup/install/files/style/ui/listView.scss | 6 ++ wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 12 files changed, 269 insertions(+), 49 deletions(-) create mode 100644 com.woltlab.wcf/templates/reactionSummaryDetailsFilterButtons.tpl create mode 100644 ts/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.js diff --git a/com.woltlab.wcf/templates/reactionSummaryDetailsFilterButtons.tpl b/com.woltlab.wcf/templates/reactionSummaryDetailsFilterButtons.tpl new file mode 100644 index 00000000000..fcde2fdd7c2 --- /dev/null +++ b/com.woltlab.wcf/templates/reactionSummaryDetailsFilterButtons.tpl @@ -0,0 +1,28 @@ + + +{foreach from=$reactionTypes item='reactionType'} + +{/foreach} + + diff --git a/com.woltlab.wcf/templates/shared_listView.tpl b/com.woltlab.wcf/templates/shared_listView.tpl index 3d90074b507..7464bbf4efe 100644 --- a/com.woltlab.wcf/templates/shared_listView.tpl +++ b/com.woltlab.wcf/templates/shared_listView.tpl @@ -1,6 +1,12 @@
- {if $view->isSortable() || $view->isFilterable() || $view->hasBulkInteractions()} + {if $view->isSortable() || $view->isFilterable() || $view->hasBulkInteractions() || $view->getAdditionalHeaderContent()}
+ {if $view->getAdditionalHeaderContent()} +
+ {unsafe:$view->getAdditionalHeaderContent()} +
+ {/if} + {if $view->isFilterable()}
{foreach from=$view->getActiveFilters() item='value' key='key'} @@ -17,53 +23,57 @@ {/foreach}
{/if} -
- {if $view->hasAvailableInteractions()} -
- + {hascontent} +
+ {content} + {if $view->hasAvailableInteractions()} +
+ - {if $view->hasBulkInteractions()} - + {if $view->hasBulkInteractions()} + + {/if} +
{/if} -
- {/if} - {if $view->isSortable()} - - {/if} - {if $view->isFilterable()} -
- -
- {/if} - {if $view->getPrimaryButton()} -
- {unsafe:$view->getPrimaryButton()->render()} -
- {/if} -
+ {if $view->isSortable()} + + {/if} + {if $view->isFilterable()} +
+ +
+ {/if} + {if $view->getPrimaryButton()} +
+ {unsafe:$view->getPrimaryButton()->render()} +
+ {/if} + {/content} +
+ {/hascontent}
{/if} diff --git a/ts/WoltLabSuite/Core/Component/ListView.ts b/ts/WoltLabSuite/Core/Component/ListView.ts index cf37ad115ca..9451092aede 100644 --- a/ts/WoltLabSuite/Core/Component/ListView.ts +++ b/ts/WoltLabSuite/Core/Component/ListView.ts @@ -128,6 +128,22 @@ export class ListView { this.#viewElement.addEventListener("interaction:reset-selection", () => { this.#state.resetSelection(); }); + + this.#viewElement.addEventListener("interaction:set-parameters", (event: CustomEvent) => { + for (const key of Object.keys(event.detail)) { + if (this.#listViewParameters === undefined) { + this.#listViewParameters = new Map(); + } + + if (event.detail[key] === undefined) { + this.#listViewParameters.delete(key); + } else { + this.#listViewParameters.set(key, event.detail[key]); + } + } + + void this.#loadItems(StateChangeCause.Change); + }); } #setupState( diff --git a/ts/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.ts b/ts/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.ts new file mode 100644 index 00000000000..d0ba2e40c27 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.ts @@ -0,0 +1,35 @@ +/** + * Handles the filter buttons in the reaction summary details dialog. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +export function setup(listViewId: string): void { + document + .querySelectorAll(`[data-filter-reaction-type-id][data-list-view-id="${listViewId}"]`) + .forEach((button) => { + button.addEventListener("click", () => { + if (button.classList.contains("active")) { + return; + } + + document + .querySelectorAll(`[data-filter-reaction-type-id][data-list-view-id="${listViewId}"]`) + .forEach((button) => { + button.classList.remove("active"); + }); + + button.classList.add("active"); + + let reactionTypeID: string | undefined = undefined; + if (button.dataset.filterReactionTypeId && button.dataset.filterReactionTypeId !== "0") { + reactionTypeID = button.dataset.filterReactionTypeId; + } + const listView = document.getElementById(`${listViewId}_items`); + listView!.dispatchEvent(new CustomEvent("interaction:set-parameters", { detail: { reactionTypeID } })); + }); + }); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js index a069f456cdd..5cfe83c5af3 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js @@ -84,6 +84,20 @@ define(["require", "exports", "tslib", "./ListView/State", "../Dom/Change/Listen this.#viewElement.addEventListener("interaction:reset-selection", () => { this.#state.resetSelection(); }); + this.#viewElement.addEventListener("interaction:set-parameters", (event) => { + for (const key of Object.keys(event.detail)) { + if (this.#listViewParameters === undefined) { + this.#listViewParameters = new Map(); + } + if (event.detail[key] === undefined) { + this.#listViewParameters.delete(key); + } + else { + this.#listViewParameters.set(key, event.detail[key]); + } + } + void this.#loadItems(0 /* StateChangeCause.Change */); + }); } #setupState(viewId, pageNo, baseUrl, sortField, sortOrder, defaultSortField, defaultSortOrder) { const state = new State_1.default(viewId, this.#viewElement, pageNo, baseUrl, sortField, sortOrder, defaultSortField, defaultSortOrder); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.js new file mode 100644 index 00000000000..5b31041b248 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Reaction/SummaryDetailsFilterButtons.js @@ -0,0 +1,36 @@ +/** + * Handles the filter buttons in the reaction summary details dialog. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setup = setup; + function setup(listViewId) { + document + .querySelectorAll(`[data-filter-reaction-type-id][data-list-view-id="${listViewId}"]`) + .forEach((button) => { + button.addEventListener("click", () => { + if (button.classList.contains("active")) { + return; + } + document + .querySelectorAll(`[data-filter-reaction-type-id][data-list-view-id="${listViewId}"]`) + .forEach((button) => { + button.classList.remove("active"); + }); + button.classList.add("active"); + let reactionTypeID = undefined; + if (button.dataset.filterReactionTypeId && button.dataset.filterReactionTypeId !== "0") { + reactionTypeID = button.dataset.filterReactionTypeId; + } + const listView = document.getElementById(`${listViewId}_items`); + listView.dispatchEvent(new CustomEvent("interaction:set-parameters", { detail: { reactionTypeID } })); + }); + }); + } +}); diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index a1f14174298..734b0efb299 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -162,6 +162,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.reactions.react'); $event->preload('wcf.reactions.summary.listReactions'); + $event->preload('wcf.reactions.summary.title'); $event->preload('wcf.style.changeStyle'); diff --git a/wcfsetup/install/files/lib/system/listView/AbstractListView.class.php b/wcfsetup/install/files/lib/system/listView/AbstractListView.class.php index 6a7bc922db9..02028b58028 100644 --- a/wcfsetup/install/files/lib/system/listView/AbstractListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/AbstractListView.class.php @@ -54,6 +54,7 @@ abstract class AbstractListView private int $fixedNumberOfItems = 0; private string $markAsReadEndpoint = ''; private ?ListViewPrimaryButton $primaryButton = null; + private string $additionalHeaderContent = ''; /** * @var array @@ -816,6 +817,22 @@ private function init(): void } } + /** + * @since 6.3 + */ + public function setAdditionalHeaderContent(string $content): void + { + $this->additionalHeaderContent = $content; + } + + /** + * @since 6.3 + */ + public function getAdditionalHeaderContent(): string + { + return $this->additionalHeaderContent; + } + /** * @return TDatabaseObjectList */ diff --git a/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php index 449afb85ce7..293b04d7854 100644 --- a/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php @@ -29,7 +29,8 @@ class ReactionSummaryDetailsListView extends AbstractListView { public function __construct( public readonly string $objectType, - public readonly int $objectID + public readonly int $objectID, + public readonly ?int $reactionTypeID = null, ) { $this->addAvailableSortFields([ new ListViewSortField( @@ -45,6 +46,7 @@ public function __construct( $this->setItemsPerPage(100); $this->setCssClassName('simpleUserList'); $this->setContainerCssClassName('simpleUserList__container'); + $this->setAdditionalHeaderContent($this->getSimpleFilterButtons()); } #[\Override] @@ -55,6 +57,9 @@ protected function createObjectList(): ViewableLikeList ReactionHandler::getInstance()->getObjectType($this->objectType)->objectTypeID ]); $likeList->getConditionBuilder()->add('objectID = ?', [$this->objectID]); + if ($this->reactionTypeID !== null) { + $likeList->getConditionBuilder()->add('reactionTypeID = ?', [$this->reactionTypeID]); + } return $likeList; } @@ -102,8 +107,6 @@ public function renderInteractionContextMenuButton(DatabaseObject $item): string return ''; } - \assert($item instanceof ViewableLike); - return $this->getInteractionContextMenuComponent()->renderButton($item->getUserProfile()); } @@ -112,4 +115,56 @@ protected function getInitializedEvent(): ReactionSummaryDetailsListViewInitiali { return new ReactionSummaryDetailsListViewInitialized($this); } + + #[\Override] + public function getParameters(): array + { + $parameters = [ + 'objectType' => $this->objectType, + 'objectID' => $this->objectID, + ]; + + if ($this->reactionTypeID !== null) { + $parameters['reactionTypeID'] = $this->reactionTypeID; + } + + return $parameters; + } + + private function getSimpleFilterButtons(): string + { + $objectType = ReactionHandler::getInstance()->getObjectType($this->objectType); + if ($objectType === null) { + return ''; + } + + $sql = "SELECT COUNT(*) AS count, reactionTypeID FROM wcf1_like WHERE objectTypeID = ? AND objectID = ? GROUP BY reactionTypeID"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$objectType->objectTypeID, $this->objectID]); + $reactionCounts = $statement->fetchMap('reactionTypeID', 'count'); + if (\count($reactionCounts) <= 1) { + // Skip filtering if only one type is present. + return ''; + } + + $totalCount = 0; + foreach ($reactionCounts as $count) { + $totalCount += $count; + } + + return WCF::getTPL()->render( + 'wcf', + 'reactionSummaryDetailsFilterButtons', + [ + 'view' => $this, + 'totalCount' => $totalCount, + 'reactionCounts' => $reactionCounts, + 'reactionTypes' => \array_filter( + ReactionHandler::getInstance()->getReactionTypes(), + static fn($reactionType) => isset($reactionCounts[$reactionType->reactionTypeID]) + ), + 'reactionTypeID' => $this->reactionTypeID, + ] + ); + } } diff --git a/wcfsetup/install/files/style/ui/listView.scss b/wcfsetup/install/files/style/ui/listView.scss index e090e9251cf..eecdad010fe 100644 --- a/wcfsetup/install/files/style/ui/listView.scss +++ b/wcfsetup/install/files/style/ui/listView.scss @@ -19,6 +19,12 @@ } } +.listView__header__additionalContent { + display: flex; + gap: 5px; + flex-wrap: wrap; +} + .listView__filters { display: flex; gap: 5px; diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index ce3823bb978..cfcf6c1620c 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4224,6 +4224,7 @@ Erlaubte Dateiendungen: gif, jpg, jpeg, png, webp]]> getTitle()} × {#$count}{if $other} und {if $other == 1}eine weitere Reaktion{else}{#$other} weitere Reaktionen{/if}{/if}]]> + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 3f1ec1beb24..2e0366cfc22 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4170,6 +4170,7 @@ Allowed extensions: gif, jpg, jpeg, png, webp]]> getTitle()} × {#$count}{if $other} and {if $other == 1}one other reaction{else}{#$other} other reactions{/if}{/if}]]> + From 78a86b1ce9656ecc4db6a594dfc3cd73215e761c Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sat, 2 May 2026 19:38:26 +0200 Subject: [PATCH 3/3] Move follow button into the interaction drop down --- .../reactionSummaryDetailsListItems.tpl | 22 ------ .../user/UserProfileInteractions.class.php | 2 +- ...serProfileInteractionsWithFollow.class.php | 71 +++++++++++++++++++ .../ReactionSummaryDetailsListView.class.php | 4 +- 4 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractionsWithFollow.class.php diff --git a/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl b/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl index 06db73d09f4..df7006f0d4a 100644 --- a/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl +++ b/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl @@ -23,28 +23,6 @@ {unsafe:$reaction->render()}
- {if $__wcf->user->userID && $user->userID != $__wcf->user->userID} - {if !$__wcf->getUserProfileHandler()->isIgnoredByUser($user->userID)} - {if $__wcf->getUserProfileHandler()->isFollowing($user->userID)} - - {else} - - {/if} - {/if} - {/if} - {unsafe:$view->renderInteractionContextMenuButton($reaction)}
diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractions.class.php index 2f36da5ffbd..50009bd6cb7 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractions.class.php @@ -22,7 +22,7 @@ * @license GNU Lesser General Public License * @since 6.2 */ -final class UserProfileInteractions extends AbstractInteractionProvider +class UserProfileInteractions extends AbstractInteractionProvider { public function __construct() { diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractionsWithFollow.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractionsWithFollow.class.php new file mode 100644 index 00000000000..4ff8e8e3191 --- /dev/null +++ b/wcfsetup/install/files/lib/system/interaction/user/UserProfileInteractionsWithFollow.class.php @@ -0,0 +1,71 @@ + + * @since 6.3 + */ +final class UserProfileInteractionsWithFollow extends UserProfileInteractions +{ + public function __construct() + { + $this->addInteractions([ + new class( + 'follow', + static fn(UserProfile $user) => !WCF::getUser()->isGuest() + && WCF::getUser()->userID !== $user->userID + && !UserProfileHandler::getInstance()->isIgnoredByUser($user->userID) + ) extends AbstractInteraction { + #[\Override] + public function render(DatabaseObject $object): string + { + \assert($object instanceof UserProfile); + + $endpoint = StringUtil::encodeHTML( + LinkHandler::getInstance()->getControllerLink(UserFollowAction::class, ['id' => $object->userID]) + ); + + if (UserProfileHandler::getInstance()->isFollowing($object->userID)) { + $title = WCF::getLanguage()->get('wcf.user.button.unfollow'); + + return <<{$title} + HTML; + } else { + $title = WCF::getLanguage()->get('wcf.user.button.follow'); + + return <<{$title} + HTML; + } + } + }, + ]); + + parent::__construct(); + } +} diff --git a/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php index 293b04d7854..09a8079c845 100644 --- a/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php @@ -9,7 +9,7 @@ use wcf\data\like\ViewableLike; use wcf\data\like\ViewableLikeList; use wcf\event\listView\user\ReactionSummaryDetailsListViewInitialized; -use wcf\system\interaction\user\UserProfileInteractions; +use wcf\system\interaction\user\UserProfileInteractionsWithFollow; use wcf\system\listView\AbstractListView; use wcf\system\listView\ListViewSortField; use wcf\system\reaction\ReactionHandler; @@ -41,7 +41,7 @@ public function __construct( ]); $this->setAllowSorting(false); - $this->setInteractionProvider(new UserProfileInteractions()); + $this->setInteractionProvider(new UserProfileInteractionsWithFollow()); $this->setDefaultSortField('username'); $this->setItemsPerPage(100); $this->setCssClassName('simpleUserList');