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/reactionSummaryDetailsListItems.tpl b/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl
new file mode 100644
index 00000000000..df7006f0d4a
--- /dev/null
+++ b/com.woltlab.wcf/templates/reactionSummaryDetailsListItems.tpl
@@ -0,0 +1,30 @@
+{foreach from=$view->getItems() item='reaction'}
+ {assign var='user' value=$reaction->getUserProfile()}
+
+
+ {unsafe:$user->getAvatar()->getImageTag(96)}
+
+
+
+
+
+ {if MODULE_USER_RANK && $user->getUserTitle()}
+
{$user->getUserTitle()}
+ {/if}
+
+
+ {time time=$reaction->time}
+
+
+
+
+
+{/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}
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/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/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/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/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/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/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/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/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/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/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/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
new file mode 100644
index 00000000000..09a8079c845
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/listView/user/ReactionSummaryDetailsListView.class.php
@@ -0,0 +1,170 @@
+
+ * @since 6.3
+ *
+ * @extends AbstractListView
+ */
+class ReactionSummaryDetailsListView extends AbstractListView
+{
+ public function __construct(
+ public readonly string $objectType,
+ public readonly int $objectID,
+ public readonly ?int $reactionTypeID = null,
+ ) {
+ $this->addAvailableSortFields([
+ new ListViewSortField(
+ 'username',
+ 'wcf.user.username',
+ '(SELECT username FROM wcf1_user WHERE userID = like_table.userID)'
+ ),
+ ]);
+
+ $this->setAllowSorting(false);
+ $this->setInteractionProvider(new UserProfileInteractionsWithFollow());
+ $this->setDefaultSortField('username');
+ $this->setItemsPerPage(100);
+ $this->setCssClassName('simpleUserList');
+ $this->setContainerCssClassName('simpleUserList__container');
+ $this->setAdditionalHeaderContent($this->getSimpleFilterButtons());
+ }
+
+ #[\Override]
+ protected function createObjectList(): ViewableLikeList
+ {
+ $likeList = new ViewableLikeList();
+ $likeList->getConditionBuilder()->add('objectTypeID = ?', [
+ ReactionHandler::getInstance()->getObjectType($this->objectType)->objectTypeID
+ ]);
+ $likeList->getConditionBuilder()->add('objectID = ?', [$this->objectID]);
+ if ($this->reactionTypeID !== null) {
+ $likeList->getConditionBuilder()->add('reactionTypeID = ?', [$this->reactionTypeID]);
+ }
+
+ 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 '';
+ }
+
+ return $this->getInteractionContextMenuComponent()->renderButton($item->getUserProfile());
+ }
+
+ #[\Override]
+ protected function getInitializedEvent(): ReactionSummaryDetailsListViewInitialized
+ {
+ 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/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;
+}
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}]]>
+