From 8a760d989986aa6e11fcabe4dfa87f933e985ac8 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Thu, 7 Aug 2025 15:35:23 +0200 Subject: [PATCH] Use 'mark as read' feature of the list view --- .../MarkAllConversationsAsRead.js | 22 +++++ .../Conversation/Component/MarkAllAsRead.js | 14 ++-- .../Core/Conversation/Component/MarkAsRead.js | 26 ------ .../com.woltlab.wcf.conversation.php | 2 + .../MarkAllConversationsAsRead.class.php | 49 +++++++++++ .../MarkConversationAsRead.class.php | 84 +++++++++++++++++++ .../MarkAllConversationsAsRead.class.php | 35 ++++++++ .../MarkConversationAsRead.class.php | 43 ++++++++++ .../user/ConversationListView.class.php | 1 + language/de.xml | 1 - language/en.xml | 1 - templates/conversationList.tpl | 8 +- templates/conversationListItems.tpl | 9 +- .../MarkAllConversationsAsRead.ts | 21 +++++ .../Conversation/Component/MarkAllAsRead.ts | 15 ++-- .../Core/Conversation/Component/MarkAsRead.ts | 34 -------- 16 files changed, 273 insertions(+), 92 deletions(-) create mode 100644 files/js/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.js delete mode 100644 files/js/WoltLabSuite/Core/Conversation/Component/MarkAsRead.js create mode 100644 files/lib/command/conversation/MarkAllConversationsAsRead.class.php create mode 100644 files/lib/command/conversation/MarkConversationAsRead.class.php create mode 100644 files/lib/system/endpoint/controller/core/conversations/MarkAllConversationsAsRead.class.php create mode 100644 files/lib/system/endpoint/controller/core/conversations/MarkConversationAsRead.class.php create mode 100644 ts/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.ts delete mode 100644 ts/WoltLabSuite/Core/Conversation/Component/MarkAsRead.ts diff --git a/files/js/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.js b/files/js/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.js new file mode 100644 index 00000000..45a794e6 --- /dev/null +++ b/files/js/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.js @@ -0,0 +1,22 @@ +/** + * Marks all conversations as read. + * + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ +define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.markAllConversationsAsRead = markAllConversationsAsRead; + async function markAllConversationsAsRead() { + try { + await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/mark-all-as-read`).post().fetchAsJson(); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)([]); + } +}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.js b/files/js/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.js index a2a20339..76f9ad83 100644 --- a/files/js/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.js +++ b/files/js/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.js @@ -6,22 +6,20 @@ * @license GNU Lesser General Public License * @since 6.2 */ -define(["require", "exports", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, Ajax_1, Snackbar_1, PromiseMutex_1) { +define(["require", "exports", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Helper/PromiseMutex", "../../Api/Conversations/MarkAllConversationsAsRead"], function (require, exports, Snackbar_1, PromiseMutex_1, MarkAllConversationsAsRead_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; - async function markAllAsRead() { - await (0, Ajax_1.dboAction)("markAllAsRead", "wcf\\data\\conversation\\ConversationAction").dispatch(); - document.querySelectorAll(".conversationList__item__markAsRead").forEach((element) => { - element.remove(); - }); + async function markAllAsRead(listView) { + (await (0, MarkAllConversationsAsRead_1.markAllConversationsAsRead)()).unwrap(); + listView.dispatchEvent(new CustomEvent("interaction:invalidate-all")); document.querySelector("#unreadConversations .badgeUpdate")?.remove(); (0, Snackbar_1.showDefaultSuccessSnackbar)(); } - function setup() { + function setup(listView) { document.querySelectorAll(".markAllAsReadButton").forEach((element) => { element.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - await markAllAsRead(); + await markAllAsRead(listView); })); }); } diff --git a/files/js/WoltLabSuite/Core/Conversation/Component/MarkAsRead.js b/files/js/WoltLabSuite/Core/Conversation/Component/MarkAsRead.js deleted file mode 100644 index 872cda42..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Component/MarkAsRead.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Handles the mark as read button for single conversations. - * - * @author Marcel Werk - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Helper/Selector"], function (require, exports, Ajax_1, Snackbar_1, PromiseMutex_1, Selector_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.setup = setup; - async function markAsRead(button) { - const objectId = parseInt(button.dataset.objectId, 10); - await (0, Ajax_1.dboAction)("markAsRead", "wcf\\data\\conversation\\ConversationAction").objectIds([objectId]).dispatch(); - button.remove(); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } - function setup() { - (0, Selector_1.wheneverFirstSeen)(".conversationList__item__markAsRead", (element) => { - element.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - await markAsRead(element); - })); - }); - } -}); diff --git a/files/lib/bootstrap/com.woltlab.wcf.conversation.php b/files/lib/bootstrap/com.woltlab.wcf.conversation.php index 0b6adc0c..cdc728bd 100644 --- a/files/lib/bootstrap/com.woltlab.wcf.conversation.php +++ b/files/lib/bootstrap/com.woltlab.wcf.conversation.php @@ -65,6 +65,8 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\conversations\OpenConversation()); $event->register(new \wcf\system\endpoint\controller\core\conversations\CloseConversation()); $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationHeaderTitle()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\MarkAllConversationsAsRead()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\MarkConversationAsRead()); $event->register(new \wcf\system\endpoint\controller\core\conversations\RemoveConversationParticipant()); $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationParticipantList()); $event->register(new \wcf\system\endpoint\controller\core\conversations\labels\DeleteConversationLabel()); diff --git a/files/lib/command/conversation/MarkAllConversationsAsRead.class.php b/files/lib/command/conversation/MarkAllConversationsAsRead.class.php new file mode 100644 index 00000000..c82aa9c8 --- /dev/null +++ b/files/lib/command/conversation/MarkAllConversationsAsRead.class.php @@ -0,0 +1,49 @@ + + * @since 6.2 + */ +final class MarkAllConversationsAsRead +{ + public function __construct( + public readonly User $user + ) {} + + public function __invoke(): void + { + $sql = "UPDATE wcf1_conversation_to_user + SET lastVisitTime = ? + WHERE participantID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([ + \TIME_NOW, + $this->user->userID, + ]); + + UserStorageHandler::getInstance()->reset([$this->user->userID], 'unreadConversationCount'); + + UserNotificationHandler::getInstance()->markAsConfirmed( + 'conversation', + 'com.woltlab.wcf.conversation.notification', + [$this->user->userID] + ); + + UserNotificationHandler::getInstance()->markAsConfirmed( + 'conversationMessage', + 'com.woltlab.wcf.conversation.message.notification', + [$this->user->userID] + ); + } +} diff --git a/files/lib/command/conversation/MarkConversationAsRead.class.php b/files/lib/command/conversation/MarkConversationAsRead.class.php new file mode 100644 index 00000000..cbb746e8 --- /dev/null +++ b/files/lib/command/conversation/MarkConversationAsRead.class.php @@ -0,0 +1,84 @@ + + * @since 6.2 + */ +final class MarkConversationAsRead +{ + public function __construct( + public readonly Conversation $conversation, + public readonly User $user + ) {} + + public function __invoke(): void + { + $sql = "UPDATE wcf1_conversation_to_user + SET lastVisitTime = ? + WHERE participantID = ? + AND conversationID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([ + \TIME_NOW, + $this->user->userID, + $this->conversation->conversationID, + ]); + + UserStorageHandler::getInstance()->reset([$this->user->userID], 'unreadConversationCount'); + + $this->markNotificationsAsConfirmed($this->conversation->conversationID, $this->user->userID); + } + + private function markNotificationsAsConfirmed(int $conversationID, int $userID): void + { + // 1) Mark notifications about new conversations as read. + UserNotificationHandler::getInstance()->markAsConfirmed( + 'conversation', + 'com.woltlab.wcf.conversation.notification', + [$userID], + [$conversationID] + ); + + // 2) Mark notifications about new replies as read. + $eventID = UserNotificationHandler::getInstance() + ->getEvent('com.woltlab.wcf.conversation.message.notification', 'conversationMessage') + ->eventID; + + $condition = new PreparedStatementConditionBuilder(); + $condition->add('notification.userID = ?', [$userID]); + $condition->add('notification.confirmTime = ?', [0]); + $condition->add('notification.eventID = ?', [$eventID]); + $condition->add("notification.objectID IN ( + SELECT messageID + FROM wcf1_conversation_message + WHERE conversationID = ? + AND time <= ? + )", [ + $conversationID, + \TIME_NOW, + ]); + + $sql = "SELECT notificationID + FROM wcf1_user_notification notification + {$condition}"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($condition->getParameters()); + + UserNotificationHandler::getInstance()->markAsConfirmedByIDs( + $statement->fetchAll(\PDO::FETCH_COLUMN) + ); + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/MarkAllConversationsAsRead.class.php b/files/lib/system/endpoint/controller/core/conversations/MarkAllConversationsAsRead.class.php new file mode 100644 index 00000000..6947ee5e --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/MarkAllConversationsAsRead.class.php @@ -0,0 +1,35 @@ + + * @since 6.2 + */ +#[PostRequest('/core/conversations/mark-all-as-read')] +final class MarkAllConversationsAsRead implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + if (!WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + + (new \wcf\command\conversation\MarkAllConversationsAsRead(WCF::getUser()))(); + + return new JsonResponse([]); + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/MarkConversationAsRead.class.php b/files/lib/system/endpoint/controller/core/conversations/MarkConversationAsRead.class.php new file mode 100644 index 00000000..fb4f26b2 --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/MarkConversationAsRead.class.php @@ -0,0 +1,43 @@ + + * @since 6.2 + */ +#[PostRequest('/core/conversations/{id:\d+}/mark-as-read')] +final class MarkConversationAsRead implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); + $this->assertConversationIsAccessible($conversation); + + (new \wcf\command\conversation\MarkConversationAsRead($conversation, WCF::getUser()))(); + + return new JsonResponse([]); + } + + private function assertConversationIsAccessible(Conversation $conversation): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + } +} diff --git a/files/lib/system/listView/user/ConversationListView.class.php b/files/lib/system/listView/user/ConversationListView.class.php index 2505aa64..27007b5e 100644 --- a/files/lib/system/listView/user/ConversationListView.class.php +++ b/files/lib/system/listView/user/ConversationListView.class.php @@ -65,6 +65,7 @@ public function __construct(string $filter = '') $this->setSortOrder(\CONVERSATION_LIST_DEFAULT_SORT_ORDER); $this->setCssClassName("discussionList conversationList"); $this->setContainerCssClassName('discussionList__container conversationList__container'); + $this->setMarkAsReadEndpoints('core/conversations/%s/mark-as-read'); } #[\Override] diff --git a/language/de.xml b/language/de.xml index 32731896..dc2f779d 100644 --- a/language/de.xml +++ b/language/de.xml @@ -121,7 +121,6 @@ - diff --git a/language/en.xml b/language/en.xml index 5783041f..cbecf8d5 100644 --- a/language/en.xml +++ b/language/en.xml @@ -121,7 +121,6 @@ - diff --git a/templates/conversationList.tpl b/templates/conversationList.tpl index d03bacfa..3c5e0548 100644 --- a/templates/conversationList.tpl +++ b/templates/conversationList.tpl @@ -93,12 +93,8 @@ diff --git a/templates/conversationListItems.tpl b/templates/conversationListItems.tpl index 7ac90a10..4aeca1b7 100644 --- a/templates/conversationListItems.tpl +++ b/templates/conversationListItems.tpl @@ -48,14 +48,7 @@
{if $conversation->isNew()} - + {unsafe:$view->renderMarkAsReadButton($conversation)} {/if}

diff --git a/ts/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.ts b/ts/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.ts new file mode 100644 index 00000000..fc122bc8 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Conversations/MarkAllConversationsAsRead.ts @@ -0,0 +1,21 @@ +/** + * Marks all conversations as read. + * + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; + +export async function markAllConversationsAsRead(): Promise> { + try { + await prepareRequest(`${window.WSC_RPC_API_URL}core/conversations/mark-all-as-read`).post().fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.ts b/ts/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.ts index f99f65c7..d61f6e0b 100644 --- a/ts/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.ts +++ b/ts/WoltLabSuite/Core/Conversation/Component/MarkAllAsRead.ts @@ -7,27 +7,26 @@ * @since 6.2 */ -import { dboAction } from "WoltLabSuite/Core/Ajax"; import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; +import { markAllConversationsAsRead } from "../../Api/Conversations/MarkAllConversationsAsRead"; -async function markAllAsRead(): Promise { - await dboAction("markAllAsRead", "wcf\\data\\conversation\\ConversationAction").dispatch(); +async function markAllAsRead(listView: HTMLElement): Promise { + (await markAllConversationsAsRead()).unwrap(); + + listView.dispatchEvent(new CustomEvent("interaction:invalidate-all")); - document.querySelectorAll(".conversationList__item__markAsRead").forEach((element: HTMLElement) => { - element.remove(); - }); document.querySelector("#unreadConversations .badgeUpdate")?.remove(); showDefaultSuccessSnackbar(); } -export function setup(): void { +export function setup(listView: HTMLElement): void { document.querySelectorAll(".markAllAsReadButton").forEach((element: HTMLElement) => { element.addEventListener( "click", promiseMutex(async () => { - await markAllAsRead(); + await markAllAsRead(listView); }), ); }); diff --git a/ts/WoltLabSuite/Core/Conversation/Component/MarkAsRead.ts b/ts/WoltLabSuite/Core/Conversation/Component/MarkAsRead.ts deleted file mode 100644 index 82b6b8e8..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Component/MarkAsRead.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Handles the mark as read button for single conversations. - * - * @author Marcel Werk - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { dboAction } from "WoltLabSuite/Core/Ajax"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; -import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; - -async function markAsRead(button: HTMLElement): Promise { - const objectId = parseInt(button.dataset.objectId!, 10); - - await dboAction("markAsRead", "wcf\\data\\conversation\\ConversationAction").objectIds([objectId]).dispatch(); - - button.remove(); - - showDefaultSuccessSnackbar(); -} - -export function setup(): void { - wheneverFirstSeen(".conversationList__item__markAsRead", (element) => { - element.addEventListener( - "click", - promiseMutex(async () => { - await markAsRead(element); - }), - ); - }); -}