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;
+ }
}