diff --git a/com.woltlab.wcf/templates/articleList.tpl b/com.woltlab.wcf/templates/articleList.tpl index 67d5f0aba3a..7eb83f866f3 100644 --- a/com.woltlab.wcf/templates/articleList.tpl +++ b/com.woltlab.wcf/templates/articleList.tpl @@ -17,8 +17,20 @@ {/capture} {capture assign='contentInteractionButtons'} - {if $__wcf->user->userID} - + {if $listView->canMarkAsRead()} + + + {/if} {/capture} @@ -36,14 +48,6 @@ {unsafe:$listView->render()} -{if $__wcf->user->userID} - -{/if} - {if $canManageArticles} {include file='shared_articleAddDialog'} {/if} diff --git a/com.woltlab.wcf/templates/articleListItems.tpl b/com.woltlab.wcf/templates/articleListItems.tpl index f80f338ef62..b5b9f434ae6 100644 --- a/com.woltlab.wcf/templates/articleListItems.tpl +++ b/com.woltlab.wcf/templates/articleListItems.tpl @@ -32,7 +32,6 @@ {content} {if $article->isDeleted}{lang}wcf.message.status.deleted{/lang}{/if} {if !$article->isPublished()}{lang}wcf.message.status.disabled{/lang}{/if} - {if $article->isNew()}{lang}wcf.message.new{/lang}{/if} {event name='contentItemBadges'}{* deprecated: use badges instead *} {event name='badges'} @@ -51,6 +50,10 @@ {/if}

+ {if $article->isNew()} + {unsafe:$view->renderMarkAsReadButton($article)} + {/if} + {$article->getTitle()}

diff --git a/com.woltlab.wcf/templates/categoryArticleList.tpl b/com.woltlab.wcf/templates/categoryArticleList.tpl index 2faab6f276c..f3828a1b8f3 100644 --- a/com.woltlab.wcf/templates/categoryArticleList.tpl +++ b/com.woltlab.wcf/templates/categoryArticleList.tpl @@ -24,8 +24,20 @@ {capture assign='contentInteractionButtons'} {include file='__userObjectWatchButton' isSubscribed=$category->isSubscribed() objectType='com.woltlab.wcf.article.category' objectID=$category->categoryID} - {if $__wcf->user->userID} - + {if $listView->canMarkAsRead()} + + + {/if} {/capture} @@ -43,14 +55,6 @@ {unsafe:$listView->render()} -{if $__wcf->user->userID} - -{/if} - {if $canManageArticles} {include file='shared_articleAddDialog'} {/if} diff --git a/ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts b/ts/WoltLabSuite/Core/Component/Article/MarkAllArticlesAsRead.ts similarity index 55% rename from ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts rename to ts/WoltLabSuite/Core/Component/Article/MarkAllArticlesAsRead.ts index 09a7a556970..7b8c4f3789f 100644 --- a/ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts +++ b/ts/WoltLabSuite/Core/Component/Article/MarkAllArticlesAsRead.ts @@ -1,9 +1,10 @@ /** * Handles the 'mark as read' action for articles. * - * @author Marcel Werk - * @copyright 2001-2023 WoltLab GmbH - * @license GNU Lesser General Public License + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 * @woltlabExcludeBundle tiny */ @@ -11,7 +12,7 @@ import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar import { markAllArticlesAsRead } from "WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; -async function markAllAsRead(listView?: HTMLElement): Promise { +async function markAllAsRead(button: HTMLElement, listView?: HTMLElement): Promise { await markAllArticlesAsRead(); if (listView !== undefined) { @@ -20,16 +21,16 @@ async function markAllAsRead(listView?: HTMLElement): Promise { document.querySelectorAll(".boxMenu .active .badgeUpdate").forEach((el: HTMLElement) => el.remove()); + button.remove(); + showDefaultSuccessSnackbar(); } -export function setup(listView?: HTMLElement): void { - document.querySelectorAll(".markAllAsReadButton").forEach((el: HTMLElement) => { - el.addEventListener( - "click", - promiseMutex(async () => { - await markAllAsRead(listView); - }), - ); - }); +export function setup(button: HTMLElement, listView?: HTMLElement): void { + button.addEventListener( + "click", + promiseMutex(async () => { + await markAllAsRead(button, listView); + }), + ); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Article/MarkAllArticlesAsRead.js similarity index 65% rename from wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js rename to wcfsetup/install/files/js/WoltLabSuite/Core/Component/Article/MarkAllArticlesAsRead.js index 3d618b4c8ad..4194e5fb936 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Article/MarkAllArticlesAsRead.js @@ -1,28 +1,28 @@ /** * Handles the 'mark as read' action for articles. * - * @author Marcel Werk - * @copyright 2001-2023 WoltLab GmbH - * @license GNU Lesser General Public License + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 * @woltlabExcludeBundle tiny */ define(["require", "exports", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, Snackbar_1, MarkAllArticlesAsRead_1, PromiseMutex_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; - async function markAllAsRead(listView) { + async function markAllAsRead(button, listView) { await (0, MarkAllArticlesAsRead_1.markAllArticlesAsRead)(); if (listView !== undefined) { listView.dispatchEvent(new CustomEvent("interaction:invalidate-all")); } document.querySelectorAll(".boxMenu .active .badgeUpdate").forEach((el) => el.remove()); + button.remove(); (0, Snackbar_1.showDefaultSuccessSnackbar)(); } - function setup(listView) { - document.querySelectorAll(".markAllAsReadButton").forEach((el) => { - el.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - await markAllAsRead(listView); - })); - }); + function setup(button, listView) { + button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { + await markAllAsRead(button, listView); + })); } }); diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index ca10936e1d3..73d11e7a8f0 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -194,6 +194,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\articles\PublishArticle()); $event->register(new \wcf\system\endpoint\controller\core\articles\UnpublishArticle()); $event->register(new \wcf\system\endpoint\controller\core\articles\MarkAllArticlesAsRead()); + $event->register(new \wcf\system\endpoint\controller\core\articles\MarkArticleAsRead()); $event->register(new \wcf\system\endpoint\controller\core\articles\contents\GetArticleContentHeaderTitle()); $event->register(new \wcf\system\endpoint\controller\core\attachments\DeleteAttachment()); $event->register(new \wcf\system\endpoint\controller\core\cronjobs\EnableCronjob()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/MarkArticleAsRead.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/MarkArticleAsRead.class.php new file mode 100644 index 00000000000..f267c0d88ef --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/MarkArticleAsRead.class.php @@ -0,0 +1,48 @@ + + * @since 6.3 + */ +#[PostRequest('/core/articles/{id:\d+}/mark-as-read')] +final class MarkArticleAsRead implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + if (!MODULE_ARTICLE) { + throw new IllegalLinkException(); + } + + $article = Helper::fetchObjectFromRequestParameter($variables['id'], Article::class); + $this->assertArticleIsAccessible($article); + + (new \wcf\command\article\MarkArticleAsRead($article))(); + + return new JsonResponse([]); + } + + private function assertArticleIsAccessible(Article $article): void + { + if (!WCF::getUser()->userID || !$article->canRead()) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php b/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php index 9611ec5553a..42cd9a2875d 100644 --- a/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/ArticleListView.class.php @@ -57,6 +57,7 @@ public function __construct() $this->setDefaultSortOrder(\ARTICLE_SORT_ORDER); $this->setCssClassName('entryCardList articleList'); $this->setContainerCssClassName('entryCardList__container'); + $this->setMarkAsReadEndpoint('core/articles/%s/mark-as-read'); } #[\Override] @@ -184,4 +185,13 @@ protected function getInitializedEvent(): ArticleListViewInitialized { return new ArticleListViewInitialized($this); } + + public function canMarkAsRead(): bool + { + if (!WCF::getUser()->userID) { + return false; + } + + return ViewableArticle::getUnreadArticles() > 0; + } }