diff --git a/ts/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.ts b/ts/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.ts deleted file mode 100644 index 36c65a280d9..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.ts +++ /dev/null @@ -1,431 +0,0 @@ -/** - * Handles article trash, restore and delete. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @deprecated 6.2 No longer in use. - */ - -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import * as Ajax from "../../../Ajax"; -import { AjaxCallbackSetup, DatabaseObjectActionResponse } from "../../../Ajax/Data"; -import { confirmationFactory } from "../../../Component/Confirmation"; -import * as ControllerClipboard from "../../../Controller/Clipboard"; -import * as Core from "../../../Core"; -import DomUtil from "../../../Dom/Util"; -import * as EventHandler from "../../../Event/Handler"; -import * as Language from "../../../Language"; -import UiDialog from "../../../Ui/Dialog"; - -interface InlineEditorOptions { - i18n: { - defaultLanguageId: number; - isI18n: boolean; - languages: { - [key: string]: string; - }; - }; - redirectUrl: string; -} - -interface ArticleData { - buttons: { - delete: HTMLButtonElement; - restore: HTMLButtonElement; - trash: HTMLButtonElement; - }; - element: HTMLElement | undefined; - isArticleEdit: boolean; -} - -interface ClipboardResponseData { - objectIDs: number[]; -} - -interface ClipboardActionData { - data: { - actionName: string; - internalData: { - template: string; - }; - }; - responseData: ClipboardResponseData | null; -} - -const articles = new Map(); - -class AcpUiArticleInlineEditor { - private readonly options: InlineEditorOptions; - - /** - * Initializes the ACP inline editor for articles. - */ - constructor(objectId: number, options: InlineEditorOptions) { - this.options = Core.extend( - { - i18n: { - defaultLanguageId: 0, - isI18n: false, - languages: {}, - }, - redirectUrl: "", - }, - options, - ) as InlineEditorOptions; - - if (objectId) { - this.initArticle(undefined, objectId); - } else { - document.querySelectorAll(".jsArticleRow").forEach((article: HTMLElement) => this.initArticle(article, 0)); - - EventHandler.add("com.woltlab.wcf.clipboard", "com.woltlab.wcf.article", (data) => this.clipboardAction(data)); - } - } - - /** - * Reacts to executed clipboard actions. - */ - private clipboardAction(actionData: ClipboardActionData): void { - // only consider events if the action has been executed - if (actionData.responseData !== null) { - const callbackFunction = new Map([ - ["com.woltlab.wcf.article.delete", (articleId: number) => this.triggerDelete(articleId)], - ["com.woltlab.wcf.article.publish", (articleId: number) => this.triggerPublish(articleId)], - ["com.woltlab.wcf.article.restore", (articleId: number) => this.triggerRestore(articleId)], - ["com.woltlab.wcf.article.trash", (articleId: number) => this.triggerTrash(articleId)], - ["com.woltlab.wcf.article.unpublish", (articleId: number) => this.triggerUnpublish(articleId)], - ]); - - const triggerFunction = callbackFunction.get(actionData.data.actionName); - if (triggerFunction) { - actionData.responseData.objectIDs.forEach((objectId) => triggerFunction(objectId)); - - showDefaultSuccessSnackbar(); - } - } else if (actionData.data.actionName === "com.woltlab.wcf.article.setCategory") { - const dialog = UiDialog.openStatic("articleCategoryDialog", actionData.data.internalData.template, { - title: Language.get("wcf.acp.article.setCategory"), - }); - - const submitButton = dialog.content.querySelector("[data-type=submit]") as HTMLButtonElement; - submitButton.addEventListener("click", (ev) => this.submitSetCategory(ev, dialog.content)); - } - } - - /** - * Is called, if the set category dialog form is submitted. - */ - private submitSetCategory(event: MouseEvent, content: HTMLElement): void { - event.preventDefault(); - - const innerError = content.querySelector(".innerError"); - const select = content.querySelector("select[name=categoryID]") as HTMLSelectElement; - - const categoryId = parseInt(select.value); - if (categoryId) { - Ajax.api(this, { - actionName: "setCategory", - parameters: { - categoryID: categoryId, - useMarkedArticles: true, - }, - }); - - if (innerError) { - innerError.remove(); - } - - UiDialog.close("articleCategoryDialog"); - } else if (!innerError) { - DomUtil.innerError(select, Language.get("wcf.global.form.error.empty")); - } - } - - /** - * Initializes an article row element. - */ - private initArticle(article: HTMLElement | undefined, objectId: number): void { - let isArticleEdit = false; - let title: string; - if (!article && objectId > 0) { - isArticleEdit = true; - article = undefined; - } else { - objectId = parseInt(article!.dataset.objectId!); - title = article!.dataset.title!; - } - - const scope = article || document; - - if (isArticleEdit) { - const languageId = this.options.i18n.isI18n ? this.options.i18n.defaultLanguageId : 0; - const inputField = document.getElementById(`title${languageId}`) as HTMLInputElement; - title = inputField.value; - } - - const buttonDelete = scope.querySelector(".jsButtonDelete") as HTMLButtonElement; - buttonDelete.addEventListener("click", async () => { - const result = await confirmationFactory().delete(title); - if (result) { - this.invoke(objectId, "delete"); - } - }); - - const buttonRestore = scope.querySelector(".jsButtonRestore") as HTMLButtonElement; - buttonRestore.addEventListener("click", async () => { - const result = await confirmationFactory().restore(title); - if (result) { - this.invoke(objectId, "restore"); - } - }); - - const buttonTrash = scope.querySelector(".jsButtonTrash") as HTMLButtonElement; - buttonTrash.addEventListener("click", async () => { - const { result } = await confirmationFactory().softDelete(title, false); - - if (result) { - this.invoke(objectId, "trash"); - } - }); - - if (isArticleEdit) { - const buttonToggleI18n = scope.querySelector(".jsButtonToggleI18n") as HTMLButtonElement; - if (buttonToggleI18n !== null) { - buttonToggleI18n.addEventListener("click", () => void this.toggleI18n(objectId)); - } - } - - articles.set(objectId, { - buttons: { - delete: buttonDelete, - restore: buttonRestore, - trash: buttonTrash, - }, - element: article, - isArticleEdit: isArticleEdit, - }); - } - - /** - * Toggles an article between i18n and monolingual. - */ - private async toggleI18n(objectId: number): Promise { - const phraseType = this.options.i18n.isI18n ? "convertFromI18n" : "convertToI18n"; - const phrase = Language.get(`wcf.article.${phraseType}.question`); - - let languageSelection: HTMLDListElement | undefined; - if (this.options.i18n.isI18n) { - const html = Object.entries(this.options.i18n.languages) - .map(([languageId, languageName]) => { - return ``; - }) - .join(""); - - languageSelection = document.createElement("dl"); - languageSelection.innerHTML = ` -
${Language.get("wcf.acp.article.i18n.source")}
-
${html}
- `; - } - - const { result } = await confirmationFactory() - .custom(phrase) - .withFormElements((dialog) => { - const p = document.createElement("p"); - p.innerHTML = Language.get(`wcf.article.${phraseType}.description`); - dialog.content.append(p); - - if (languageSelection !== undefined) { - dialog.content.append(languageSelection); - - dialog.incomplete = true; - languageSelection.querySelectorAll('input[name="i18nLanguage"]').forEach((input: HTMLInputElement) => { - input.addEventListener("change", () => (dialog.incomplete = false), { once: true }); - }); - } - }); - - if (result) { - let languageId = 0; - if (languageSelection !== undefined) { - const input = languageSelection.querySelector("input[name='i18nLanguage']:checked") as HTMLInputElement; - languageId = parseInt(input.value); - } - - Ajax.api(this, { - actionName: "toggleI18n", - objectIDs: [objectId], - parameters: { - languageID: languageId, - }, - }); - } - } - - /** - * Invokes the selected action. - */ - private invoke(objectId: number, actionName: string): void { - Ajax.api(this, { - actionName: actionName, - objectIDs: [objectId], - }); - } - - /** - * Handles an article being deleted. - */ - private triggerDelete(articleId: number): void { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - - if (article.isArticleEdit) { - window.location.href = this.options.redirectUrl; - } else { - const tbody = article.element!.parentElement!; - article.element!.remove(); - - if (tbody.querySelector("tr") === null) { - window.location.reload(); - } - } - } - - /** - * Handles publishing an article via clipboard. - */ - private triggerPublish(articleId: number): void { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - - if (article.isArticleEdit) { - // unsupported - } else { - const notice = article.element!.querySelector(".jsUnpublishedArticle")!; - notice.remove(); - } - } - - /** - * Handles an article being restored. - */ - private triggerRestore(articleId: number): void { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - - DomUtil.hide(article.buttons.delete); - DomUtil.hide(article.buttons.restore); - DomUtil.show(article.buttons.trash); - - if (article.isArticleEdit) { - const notice = document.querySelector(".jsArticleNoticeTrash") as HTMLElement; - notice.hidden = true; - } else { - const icon = article.element!.querySelector(".jsIconDeleted")!; - icon.remove(); - } - } - - /** - * Handles an article being trashed. - */ - private triggerTrash(articleId: number): void { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - - DomUtil.show(article.buttons.delete); - DomUtil.show(article.buttons.restore); - DomUtil.hide(article.buttons.trash); - - if (article.isArticleEdit) { - const notice = document.querySelector(".jsArticleNoticeTrash") as HTMLElement; - notice.hidden = false; - } else { - const badge = document.createElement("span"); - badge.className = "badge label red jsIconDeleted"; - badge.textContent = Language.get("wcf.message.status.deleted"); - - const h3 = article.element!.querySelector(".containerHeadline > h3") as HTMLHeadingElement; - h3.insertAdjacentElement("afterbegin", badge); - } - } - - /** - * Handles unpublishing an article via clipboard. - */ - private triggerUnpublish(articleId: number): void { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - - if (article.isArticleEdit) { - // unsupported - } else { - const badge = document.createElement("span"); - badge.className = "badge jsUnpublishedArticle"; - badge.textContent = Language.get("wcf.acp.article.publicationStatus.unpublished"); - - const h3 = article.element!.querySelector(".containerHeadline > h3") as HTMLHeadingElement; - const a = h3.querySelector("a"); - - h3.insertBefore(badge, a); - h3.insertBefore(document.createTextNode(" "), a); - } - } - - _ajaxSuccess(data: DatabaseObjectActionResponse): void { - let notificationCallback; - - switch (data.actionName) { - case "delete": - this.triggerDelete(data.objectIDs[0]); - break; - - case "restore": - this.triggerRestore(data.objectIDs[0]); - break; - - case "setCategory": - case "toggleI18n": - notificationCallback = () => window.location.reload(); - break; - - case "trash": - this.triggerTrash(data.objectIDs[0]); - break; - } - - showDefaultSuccessSnackbar().addEventListener("snackbar:close", () => { - if (notificationCallback) { - notificationCallback(); - } - }); - - ControllerClipboard.reload(); - } - - _ajaxSetup(): ReturnType { - return { - data: { - className: "wcf\\data\\article\\ArticleAction", - }, - }; - } -} - -export = AcpUiArticleInlineEditor; diff --git a/ts/WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead.ts b/ts/WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead.ts new file mode 100644 index 00000000000..c69d34df902 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead.ts @@ -0,0 +1,23 @@ +/** + * Mark all articles as read + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; + +export async function markAllArticlesAsRead(): Promise> { + try { + await prepareRequest(new URL(`${window.WSC_RPC_API_URL}core/articles/mark-all-as-read`)) + .post() + .fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts b/ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts index 1df90874368..361c0196806 100644 --- a/ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts +++ b/ts/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.ts @@ -8,10 +8,10 @@ */ import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import { dboAction } from "../../Ajax"; +import { markAllArticlesAsRead } from "WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead"; async function markAllAsRead(): Promise { - await dboAction("markAllAsRead", "wcf\\data\\article\\ArticleAction").dispatch(); + await markAllArticlesAsRead(); document.querySelectorAll(".contentItemList .contentItemBadgeNew").forEach((el: HTMLElement) => el.remove()); document.querySelectorAll(".boxMenu .active .badge").forEach((el: HTMLElement) => el.remove()); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js deleted file mode 100644 index 958252b270f..00000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js +++ /dev/null @@ -1,344 +0,0 @@ -/** - * Handles article trash, restore and delete. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @deprecated 6.2 No longer in use. - */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Snackbar", "../../../Ajax", "../../../Component/Confirmation", "../../../Controller/Clipboard", "../../../Core", "../../../Dom/Util", "../../../Event/Handler", "../../../Language", "../../../Ui/Dialog"], function (require, exports, tslib_1, Snackbar_1, Ajax, Confirmation_1, ControllerClipboard, Core, Util_1, EventHandler, Language, Dialog_1) { - "use strict"; - Ajax = tslib_1.__importStar(Ajax); - ControllerClipboard = tslib_1.__importStar(ControllerClipboard); - Core = tslib_1.__importStar(Core); - Util_1 = tslib_1.__importDefault(Util_1); - EventHandler = tslib_1.__importStar(EventHandler); - Language = tslib_1.__importStar(Language); - Dialog_1 = tslib_1.__importDefault(Dialog_1); - const articles = new Map(); - class AcpUiArticleInlineEditor { - options; - /** - * Initializes the ACP inline editor for articles. - */ - constructor(objectId, options) { - this.options = Core.extend({ - i18n: { - defaultLanguageId: 0, - isI18n: false, - languages: {}, - }, - redirectUrl: "", - }, options); - if (objectId) { - this.initArticle(undefined, objectId); - } - else { - document.querySelectorAll(".jsArticleRow").forEach((article) => this.initArticle(article, 0)); - EventHandler.add("com.woltlab.wcf.clipboard", "com.woltlab.wcf.article", (data) => this.clipboardAction(data)); - } - } - /** - * Reacts to executed clipboard actions. - */ - clipboardAction(actionData) { - // only consider events if the action has been executed - if (actionData.responseData !== null) { - const callbackFunction = new Map([ - ["com.woltlab.wcf.article.delete", (articleId) => this.triggerDelete(articleId)], - ["com.woltlab.wcf.article.publish", (articleId) => this.triggerPublish(articleId)], - ["com.woltlab.wcf.article.restore", (articleId) => this.triggerRestore(articleId)], - ["com.woltlab.wcf.article.trash", (articleId) => this.triggerTrash(articleId)], - ["com.woltlab.wcf.article.unpublish", (articleId) => this.triggerUnpublish(articleId)], - ]); - const triggerFunction = callbackFunction.get(actionData.data.actionName); - if (triggerFunction) { - actionData.responseData.objectIDs.forEach((objectId) => triggerFunction(objectId)); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } - } - else if (actionData.data.actionName === "com.woltlab.wcf.article.setCategory") { - const dialog = Dialog_1.default.openStatic("articleCategoryDialog", actionData.data.internalData.template, { - title: Language.get("wcf.acp.article.setCategory"), - }); - const submitButton = dialog.content.querySelector("[data-type=submit]"); - submitButton.addEventListener("click", (ev) => this.submitSetCategory(ev, dialog.content)); - } - } - /** - * Is called, if the set category dialog form is submitted. - */ - submitSetCategory(event, content) { - event.preventDefault(); - const innerError = content.querySelector(".innerError"); - const select = content.querySelector("select[name=categoryID]"); - const categoryId = parseInt(select.value); - if (categoryId) { - Ajax.api(this, { - actionName: "setCategory", - parameters: { - categoryID: categoryId, - useMarkedArticles: true, - }, - }); - if (innerError) { - innerError.remove(); - } - Dialog_1.default.close("articleCategoryDialog"); - } - else if (!innerError) { - Util_1.default.innerError(select, Language.get("wcf.global.form.error.empty")); - } - } - /** - * Initializes an article row element. - */ - initArticle(article, objectId) { - let isArticleEdit = false; - let title; - if (!article && objectId > 0) { - isArticleEdit = true; - article = undefined; - } - else { - objectId = parseInt(article.dataset.objectId); - title = article.dataset.title; - } - const scope = article || document; - if (isArticleEdit) { - const languageId = this.options.i18n.isI18n ? this.options.i18n.defaultLanguageId : 0; - const inputField = document.getElementById(`title${languageId}`); - title = inputField.value; - } - const buttonDelete = scope.querySelector(".jsButtonDelete"); - buttonDelete.addEventListener("click", async () => { - const result = await (0, Confirmation_1.confirmationFactory)().delete(title); - if (result) { - this.invoke(objectId, "delete"); - } - }); - const buttonRestore = scope.querySelector(".jsButtonRestore"); - buttonRestore.addEventListener("click", async () => { - const result = await (0, Confirmation_1.confirmationFactory)().restore(title); - if (result) { - this.invoke(objectId, "restore"); - } - }); - const buttonTrash = scope.querySelector(".jsButtonTrash"); - buttonTrash.addEventListener("click", async () => { - const { result } = await (0, Confirmation_1.confirmationFactory)().softDelete(title, false); - if (result) { - this.invoke(objectId, "trash"); - } - }); - if (isArticleEdit) { - const buttonToggleI18n = scope.querySelector(".jsButtonToggleI18n"); - if (buttonToggleI18n !== null) { - buttonToggleI18n.addEventListener("click", () => void this.toggleI18n(objectId)); - } - } - articles.set(objectId, { - buttons: { - delete: buttonDelete, - restore: buttonRestore, - trash: buttonTrash, - }, - element: article, - isArticleEdit: isArticleEdit, - }); - } - /** - * Toggles an article between i18n and monolingual. - */ - async toggleI18n(objectId) { - const phraseType = this.options.i18n.isI18n ? "convertFromI18n" : "convertToI18n"; - const phrase = Language.get(`wcf.article.${phraseType}.question`); - let languageSelection; - if (this.options.i18n.isI18n) { - const html = Object.entries(this.options.i18n.languages) - .map(([languageId, languageName]) => { - return ``; - }) - .join(""); - languageSelection = document.createElement("dl"); - languageSelection.innerHTML = ` -
${Language.get("wcf.acp.article.i18n.source")}
-
${html}
- `; - } - const { result } = await (0, Confirmation_1.confirmationFactory)() - .custom(phrase) - .withFormElements((dialog) => { - const p = document.createElement("p"); - p.innerHTML = Language.get(`wcf.article.${phraseType}.description`); - dialog.content.append(p); - if (languageSelection !== undefined) { - dialog.content.append(languageSelection); - dialog.incomplete = true; - languageSelection.querySelectorAll('input[name="i18nLanguage"]').forEach((input) => { - input.addEventListener("change", () => (dialog.incomplete = false), { once: true }); - }); - } - }); - if (result) { - let languageId = 0; - if (languageSelection !== undefined) { - const input = languageSelection.querySelector("input[name='i18nLanguage']:checked"); - languageId = parseInt(input.value); - } - Ajax.api(this, { - actionName: "toggleI18n", - objectIDs: [objectId], - parameters: { - languageID: languageId, - }, - }); - } - } - /** - * Invokes the selected action. - */ - invoke(objectId, actionName) { - Ajax.api(this, { - actionName: actionName, - objectIDs: [objectId], - }); - } - /** - * Handles an article being deleted. - */ - triggerDelete(articleId) { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - if (article.isArticleEdit) { - window.location.href = this.options.redirectUrl; - } - else { - const tbody = article.element.parentElement; - article.element.remove(); - if (tbody.querySelector("tr") === null) { - window.location.reload(); - } - } - } - /** - * Handles publishing an article via clipboard. - */ - triggerPublish(articleId) { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - if (article.isArticleEdit) { - // unsupported - } - else { - const notice = article.element.querySelector(".jsUnpublishedArticle"); - notice.remove(); - } - } - /** - * Handles an article being restored. - */ - triggerRestore(articleId) { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - Util_1.default.hide(article.buttons.delete); - Util_1.default.hide(article.buttons.restore); - Util_1.default.show(article.buttons.trash); - if (article.isArticleEdit) { - const notice = document.querySelector(".jsArticleNoticeTrash"); - notice.hidden = true; - } - else { - const icon = article.element.querySelector(".jsIconDeleted"); - icon.remove(); - } - } - /** - * Handles an article being trashed. - */ - triggerTrash(articleId) { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - Util_1.default.show(article.buttons.delete); - Util_1.default.show(article.buttons.restore); - Util_1.default.hide(article.buttons.trash); - if (article.isArticleEdit) { - const notice = document.querySelector(".jsArticleNoticeTrash"); - notice.hidden = false; - } - else { - const badge = document.createElement("span"); - badge.className = "badge label red jsIconDeleted"; - badge.textContent = Language.get("wcf.message.status.deleted"); - const h3 = article.element.querySelector(".containerHeadline > h3"); - h3.insertAdjacentElement("afterbegin", badge); - } - } - /** - * Handles unpublishing an article via clipboard. - */ - triggerUnpublish(articleId) { - const article = articles.get(articleId); - if (!article) { - // The affected article might be hidden by the filter settings. - return; - } - if (article.isArticleEdit) { - // unsupported - } - else { - const badge = document.createElement("span"); - badge.className = "badge jsUnpublishedArticle"; - badge.textContent = Language.get("wcf.acp.article.publicationStatus.unpublished"); - const h3 = article.element.querySelector(".containerHeadline > h3"); - const a = h3.querySelector("a"); - h3.insertBefore(badge, a); - h3.insertBefore(document.createTextNode(" "), a); - } - } - _ajaxSuccess(data) { - let notificationCallback; - switch (data.actionName) { - case "delete": - this.triggerDelete(data.objectIDs[0]); - break; - case "restore": - this.triggerRestore(data.objectIDs[0]); - break; - case "setCategory": - case "toggleI18n": - notificationCallback = () => window.location.reload(); - break; - case "trash": - this.triggerTrash(data.objectIDs[0]); - break; - } - (0, Snackbar_1.showDefaultSuccessSnackbar)().addEventListener("snackbar:close", () => { - if (notificationCallback) { - notificationCallback(); - } - }); - ControllerClipboard.reload(); - } - _ajaxSetup() { - return { - data: { - className: "wcf\\data\\article\\ArticleAction", - }, - }; - } - } - return AcpUiArticleInlineEditor; -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead.js new file mode 100644 index 00000000000..8f703f9dc51 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead.js @@ -0,0 +1,24 @@ +/** + * Mark all articles as read + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ +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.markAllArticlesAsRead = markAllArticlesAsRead; + async function markAllArticlesAsRead() { + try { + await (0, Backend_1.prepareRequest)(new URL(`${window.WSC_RPC_API_URL}core/articles/mark-all-as-read`)) + .post() + .fetchAsJson(); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)([]); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js index a3f66978d41..81683e41566 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Article/MarkAllAsRead.js @@ -6,12 +6,12 @@ * @license GNU Lesser General Public License * @woltlabExcludeBundle tiny */ -define(["require", "exports", "WoltLabSuite/Core/Component/Snackbar", "../../Ajax"], function (require, exports, Snackbar_1, Ajax_1) { +define(["require", "exports", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Api/Articles/MarkAllArticlesAsRead"], function (require, exports, Snackbar_1, MarkAllArticlesAsRead_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; async function markAllAsRead() { - await (0, Ajax_1.dboAction)("markAllAsRead", "wcf\\data\\article\\ArticleAction").dispatch(); + await (0, MarkAllArticlesAsRead_1.markAllArticlesAsRead)(); document.querySelectorAll(".contentItemList .contentItemBadgeNew").forEach((el) => el.remove()); document.querySelectorAll(".boxMenu .active .badge").forEach((el) => el.remove()); (0, Snackbar_1.showDefaultSuccessSnackbar)(); diff --git a/wcfsetup/install/files/lib/acp/action/ArticleCategoryAction.class.php b/wcfsetup/install/files/lib/acp/action/ArticleCategoryAction.class.php index cf9859f71d1..bbf6a70f724 100644 --- a/wcfsetup/install/files/lib/acp/action/ArticleCategoryAction.class.php +++ b/wcfsetup/install/files/lib/acp/action/ArticleCategoryAction.class.php @@ -6,6 +6,9 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use wcf\command\article\SetArticleCategory; +use wcf\data\article\ArticleList; +use wcf\data\article\category\ArticleCategory; use wcf\data\category\CategoryNodeTree; use wcf\http\Helper; use wcf\system\exception\IllegalLinkException; @@ -43,6 +46,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface throw new IllegalLinkException(); } + $articleList = new ArticleList(); + $articleList->setObjectIDs($parameters['objectIDs']); + $articleList->readObjects(); + $form = $this->getForm(); if ($request->getMethod() === 'GET') { @@ -54,16 +61,12 @@ public function handle(ServerRequestInterface $request): ResponseInterface } $data = $form->getData()['data']; + $category = ArticleCategory::getCategory($data['categoryID']); WCF::getDB()->beginTransaction(); - $sql = "UPDATE wcf1_article - SET categoryID = ? - WHERE articleID = ?"; - $statement = WCF::getDB()->prepare($sql); - - foreach ($parameters['objectIDs'] as $articleID) { - $statement->execute([$data['categoryID'], $articleID]); + foreach ($articleList as $article) { + (new SetArticleCategory($article, $category))(); } WCF::getDB()->commitTransaction(); diff --git a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php index 94722df4c44..91be7c51bb2 100644 --- a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php @@ -2,6 +2,7 @@ namespace wcf\acp\form; +use wcf\command\article\MarkArticleAsRead; use wcf\data\article\Article; use wcf\data\article\ArticleAction; use wcf\data\article\category\ArticleCategory; @@ -573,7 +574,7 @@ public function save() // mark published article as read if ($article->publicationStatus == Article::PUBLISHED) { - (new ArticleAction([$article], 'markAsRead'))->executeAction(); + (new MarkArticleAsRead($article))(); } // call saved event diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index e3205fd1895..a560edb517f 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -179,6 +179,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\articles\RestoreArticle()); $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\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/command/article/MarkAllArticlesAsRead.class.php b/wcfsetup/install/files/lib/command/article/MarkAllArticlesAsRead.class.php new file mode 100644 index 00000000000..6f006c04df0 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/MarkAllArticlesAsRead.class.php @@ -0,0 +1,24 @@ + + * @since 6.3 + */ +final class MarkAllArticlesAsRead +{ + public function __invoke(): void + { + VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article'); + + (new ResetUserStorageForUnreadArticles([WCF::getUser()->userID]))(); + } +} diff --git a/wcfsetup/install/files/lib/command/article/MarkArticleAsRead.class.php b/wcfsetup/install/files/lib/command/article/MarkArticleAsRead.class.php new file mode 100644 index 00000000000..4895278ebe6 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/MarkArticleAsRead.class.php @@ -0,0 +1,47 @@ + + * @since 6.3 + */ +final class MarkArticleAsRead +{ + public function __construct( + private readonly Article $article, + private readonly int $visitTime = TIME_NOW + ) {} + + public function __invoke(): void + { + VisitTracker::getInstance()->trackObjectVisit( + 'com.woltlab.wcf.article', + $this->article->articleID, + $this->visitTime + ); + + (new ResetUserStorageForUnreadArticles([WCF::getUser()->userID]))(); + + $this->deleteObsoleteNotifications(); + } + + private function deleteObsoleteNotifications(): void + { + UserNotificationHandler::getInstance()->markAsConfirmed( + 'article', + 'com.woltlab.wcf.article.notification', + [WCF::getUser()->userID], + [$this->article->articleID], + ); + } +} diff --git a/wcfsetup/install/files/lib/command/article/PublishArticle.class.php b/wcfsetup/install/files/lib/command/article/PublishArticle.class.php new file mode 100644 index 00000000000..72519b0d056 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/PublishArticle.class.php @@ -0,0 +1,67 @@ + + * @since 6.3 + */ +final class PublishArticle +{ + public function __construct(private readonly Article $article) {} + + public function __invoke(): void + { + (new ArticleEditor($this->article))->update([ + 'time' => TIME_NOW, + 'publicationStatus' => Article::PUBLISHED, + 'publicationDate' => 0, + ]); + + $this->updateUserWatch($this->article); + $this->addUserActivity($this->article->articleID, $this->article->userID); + + ArticleEditor::updateArticleCounter([ + $this->article->userID => 1, + ]); + + (new ResetUserStorageForUnreadArticles())(); + + $event = new ArticlePublished($this->article); + EventHandler::getInstance()->fire($event); + } + + private function updateUserWatch(Article $article): void + { + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $article->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($article) + ); + } + + private function addUserActivity(int $articleID, int $userID): void + { + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleID, + null, + $userID, + TIME_NOW + ); + } +} diff --git a/wcfsetup/install/files/lib/command/article/ResetUserStorageForUnreadArticles.class.php b/wcfsetup/install/files/lib/command/article/ResetUserStorageForUnreadArticles.class.php new file mode 100644 index 00000000000..389cfacf5a6 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/ResetUserStorageForUnreadArticles.class.php @@ -0,0 +1,34 @@ + + * @since 6.3 + */ +final class ResetUserStorageForUnreadArticles +{ + public function __construct( + /** @var int[] */ + private readonly array $userIDs = [] + ) {} + + public function __invoke(): void + { + if ($this->userIDs === []) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } else { + UserStorageHandler::getInstance()->reset($this->userIDs, 'unreadArticles'); + UserStorageHandler::getInstance()->reset($this->userIDs, 'unreadWatchedArticles'); + UserStorageHandler::getInstance()->reset($this->userIDs, 'unreadArticlesByCategory'); + } + } +} diff --git a/wcfsetup/install/files/lib/command/article/RestoreArticle.class.php b/wcfsetup/install/files/lib/command/article/RestoreArticle.class.php new file mode 100644 index 00000000000..9a9ae42b4e5 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/RestoreArticle.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class RestoreArticle +{ + public function __construct(private readonly Article $article) {} + + public function __invoke(): void + { + (new ArticleEditor($this->article))->update(['isDeleted' => 0]); + + (new ResetUserStorageForUnreadArticles())(); + + $event = new ArticleRestored($this->article); + EventHandler::getInstance()->fire($event); + } +} diff --git a/wcfsetup/install/files/lib/command/article/SetArticleCategory.class.php b/wcfsetup/install/files/lib/command/article/SetArticleCategory.class.php new file mode 100644 index 00000000000..c2dde690dca --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/SetArticleCategory.class.php @@ -0,0 +1,33 @@ + + * @since 6.3 + */ +final class SetArticleCategory +{ + public function __construct( + private readonly Article $article, + private readonly ArticleCategory $category, + ) {} + + public function __invoke(): void + { + (new ArticleEditor($this->article))->update(['categoryID' => $this->category->categoryID]); + + $event = new ArticleCategorySet($this->article, $this->article->getCategory(), $this->category); + EventHandler::getInstance()->fire($event); + } +} diff --git a/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php b/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php new file mode 100644 index 00000000000..d170553be29 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class SoftDeleteArticle +{ + public function __construct(private readonly Article $article) {} + + public function __invoke(): void + { + (new ArticleEditor($this->article))->update(['isDeleted' => 1]); + + (new ResetUserStorageForUnreadArticles())(); + + $event = new ArticleSoftDeleted($this->article); + EventHandler::getInstance()->fire($event); + } +} diff --git a/wcfsetup/install/files/lib/command/article/UnpublishArticle.class.php b/wcfsetup/install/files/lib/command/article/UnpublishArticle.class.php new file mode 100644 index 00000000000..57e9e884d57 --- /dev/null +++ b/wcfsetup/install/files/lib/command/article/UnpublishArticle.class.php @@ -0,0 +1,54 @@ + + * @since 6.3 + */ +final class UnpublishArticle +{ + public function __construct(private readonly Article $article) {} + + public function __invoke(): void + { + (new ArticleEditor($this->article))->update(['publicationStatus' => Article::UNPUBLISHED]); + + $this->removeNotifications($this->article->articleID); + $this->removeUserActivity($this->article->articleID); + + ArticleEditor::updateArticleCounter([ + $this->article->userID => -1, + ]); + + $event = new ArticleUnpublished($this->article); + EventHandler::getInstance()->fire($event); + } + + private function removeNotifications(int $articleID): void + { + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + [$articleID] + ); + } + + private function removeUserActivity(int $articleID): void + { + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + [$articleID] + ); + } +} diff --git a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php index 43254af7388..148a83aef95 100644 --- a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php +++ b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php @@ -2,12 +2,21 @@ namespace wcf\data\article; +use wcf\command\article\MarkAllArticlesAsRead; +use wcf\command\article\MarkArticleAsRead; +use wcf\command\article\PublishArticle; +use wcf\command\article\ResetUserStorageForUnreadArticles; +use wcf\command\article\RestoreArticle; +use wcf\command\article\SetArticleCategory; +use wcf\command\article\SoftDeleteArticle; +use wcf\command\article\UnpublishArticle; use wcf\data\AbstractDatabaseObjectAction; use wcf\data\article\category\ArticleCategory; use wcf\data\article\content\ArticleContent; -use wcf\data\article\content\ArticleContentAction; use wcf\data\article\content\ArticleContentEditor; use wcf\data\language\Language; +use wcf\system\article\command\DisableI18n; +use wcf\system\article\command\EnableI18n; use wcf\system\attachment\AttachmentHandler; use wcf\system\clipboard\ClipboardHandler; use wcf\system\comment\CommentHandler; @@ -23,9 +32,7 @@ use wcf\system\user\notification\object\ArticleUserNotificationObject; use wcf\system\user\notification\UserNotificationHandler; use wcf\system\user\object\watch\UserObjectWatchHandler; -use wcf\system\user\storage\UserStorageHandler; use wcf\system\version\VersionTracker; -use wcf\system\visitTracker\VisitTracker; use wcf\system\WCF; /** @@ -149,10 +156,7 @@ public function create() } } - // reset storage - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + (new ResetUserStorageForUnreadArticles())(); if ($article->publicationStatus == Article::PUBLISHED) { ArticleEditor::updateArticleCounter([$article->userID => 1]); @@ -294,10 +298,7 @@ public function update() } } - // reset storage - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + (new ResetUserStorageForUnreadArticles())(); $publicationStatus = (isset($this->parameters['data']['publicationStatus'])) ? $this->parameters['data']['publicationStatus'] : null; if ($publicationStatus !== null) { @@ -465,8 +466,6 @@ public function delete() } } - $this->unmarkItems(); - return [ 'objectIDs' => $this->objectIDs, 'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true]), @@ -480,6 +479,8 @@ public function delete() * @return void * @throws PermissionDeniedException * @throws UserInputException + * + * @deprecated 6.3 */ public function validateTrash() { @@ -506,20 +507,15 @@ public function validateTrash() * Moves articles to the trash bin. * * @return array{objectIDs: int[]} + * + * @deprecated 6.3 use `SoftDeleteArticle` */ public function trash() { foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['isDeleted' => 1]); + (new SoftDeleteArticle($articleEditor->getDecoratedObject()))(); } - $this->unmarkItems(); - - // reset storage - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - return ['objectIDs' => $this->objectIDs]; } @@ -528,6 +524,8 @@ public function trash() * * @return void * @throws UserInputException + * + * @deprecated 6.3 */ public function validateRestore() { @@ -538,20 +536,15 @@ public function validateRestore() * Restores articles. * * @return array{objectIDs: int[]} + * + * @deprecated 6.3 use `RestoreArticle` */ public function restore() { foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['isDeleted' => 0]); + (new RestoreArticle($articleEditor->getDecoratedObject()))(); } - $this->unmarkItems(); - - // reset storage - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - return ['objectIDs' => $this->objectIDs]; } @@ -560,6 +553,8 @@ public function restore() * * @return void * @throws UserInputException + * + * @deprecated 6.3 */ public function validateToggleI18n() { @@ -585,62 +580,24 @@ public function validateToggleI18n() * Toggles between i18n and monolingual mode. * * @return void + * + * @deprecated 6.3 use `EnableI18n` or `DisableI18n` */ public function toggleI18n() { - $removeContent = []; - - // i18n -> monolingual - if ($this->articleEditor->getDecoratedObject()->isMultilingual) { - foreach ($this->articleEditor->getArticleContents() as $articleContent) { - if ($articleContent->languageID == $this->language->languageID) { - $articleContentEditor = new ArticleContentEditor($articleContent); - $articleContentEditor->update(['languageID' => null]); - } else { - $removeContent[] = $articleContent; - } - } + if ($this->articleEditor->isMultilingual) { + (new DisableI18n($this->articleEditor->getDecoratedObject(), $this->language))(); } else { - // monolingual -> i18n - $articleContent = $this->articleEditor->getArticleContent(); - $data = []; - foreach (LanguageFactory::getInstance()->getLanguages() as $language) { - $data[$language->languageID] = [ - 'title' => $articleContent->title, - 'teaser' => $articleContent->teaser, - 'content' => $articleContent->content, - 'imageID' => $articleContent->imageID ?: null, - 'teaserImageID' => $articleContent->teaserImageID ?: null, - ]; - } - - $action = new self([$this->articleEditor], 'update', ['content' => $data]); - $action->executeAction(); - - $removeContent[] = $articleContent; + (new EnableI18n($this->articleEditor->getDecoratedObject()))(); } - - if (!empty($removeContent)) { - $action = new ArticleContentAction($removeContent, 'delete'); - $action->executeAction(); - } - - // flush edit history - VersionTracker::getInstance()->reset( - 'com.woltlab.wcf.article', - $this->articleEditor->getDecoratedObject()->articleID - ); - - // update article's i18n state - $this->articleEditor->update([ - 'isMultilingual' => ($this->articleEditor->getDecoratedObject()->isMultilingual) ? 0 : 1, - ]); } /** * Marks articles as read. * * @return void + * + * @deprecated 6.3 use `MarkArticleAsRead` */ public function markAsRead() { @@ -656,28 +613,8 @@ public function markAsRead() $this->readObjects(); } - $articleIDs = []; - foreach ($this->getObjects() as $article) { - $articleIDs[] = $article->articleID; - VisitTracker::getInstance()->trackObjectVisit( - 'com.woltlab.wcf.article', - $article->articleID, - $this->parameters['visitTime'] - ); - } - - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); - - // delete obsolete notifications - if ($articleIDs !== []) { - UserNotificationHandler::getInstance()->markAsConfirmed( - 'article', - 'com.woltlab.wcf.article.notification', - [WCF::getUser()->userID], - $articleIDs - ); + foreach ($this->getObjects() as $articleEditor) { + (new MarkArticleAsRead($articleEditor->getDecoratedObject(), $this->parameters['visitTime']))(); } } @@ -685,24 +622,20 @@ public function markAsRead() * Marks all articles as read. * * @return void + * + * @deprecated 6.3 use `MarkAllArticleAsRead` */ public function markAllAsRead() { - if (!WCF::getUser()->userID) { - return; - } - - VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article'); - - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); + (new MarkAllArticlesAsRead())(); } /** * Validates the mark all as read action. * * @return void + * + * @deprecated 6.3 */ public function validateMarkAllAsRead() { @@ -714,6 +647,8 @@ public function validateMarkAllAsRead() * * @return void * @throws UserInputException + * + * @deprecated 6.3 */ public function validateSetCategory() { @@ -748,14 +683,16 @@ public function validateSetCategory() * Sets the category of articles. * * @return void + * + * @deprecated 6.3 use `SetArticleCategory` */ public function setCategory() { + $category = ArticleCategory::getCategory($this->parameters['categoryID']); + foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['categoryID' => $this->parameters['categoryID']]); + (new SetArticleCategory($articleEditor->getDecoratedObject(), $category))(); } - - $this->unmarkItems(); } /** @@ -764,6 +701,8 @@ public function setCategory() * @return void * @throws PermissionDeniedException * @throws UserInputException + * + * @deprecated 6.3 */ public function validatePublish() { @@ -790,48 +729,14 @@ public function validatePublish() * Publishes articles. * * @return void + * + * @deprecated 6.3 use `PublishArticle` */ public function publish() { - $usersToArticles = []; foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update([ - 'time' => TIME_NOW, - 'publicationStatus' => Article::PUBLISHED, - 'publicationDate' => 0, - ]); - - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID]++; - - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $articleEditor->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) - ); - - UserActivityEventHandler::getInstance()->fireEvent( - 'com.woltlab.wcf.article.recentActivityEvent', - $articleEditor->articleID, - null, - $articleEditor->userID, - TIME_NOW - ); + (new PublishArticle($articleEditor->getDecoratedObject()))(); } - - ArticleEditor::updateArticleCounter($usersToArticles); - - // reset storage - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - - $this->unmarkItems(); } /** @@ -840,6 +745,8 @@ public function publish() * @return void * @throws PermissionDeniedException * @throws UserInputException + * + * @deprecated 6.3 */ public function validateUnpublish() { @@ -866,37 +773,14 @@ public function validateUnpublish() * Unpublishes articles. * * @return void + * + * @deprecated 6.3 use `UnpublishArticle` */ public function unpublish() { - $usersToArticles = $articleIDs = []; foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['publicationStatus' => Article::UNPUBLISHED]); - - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID]--; - - $articleIDs[] = $articleEditor->articleID; + (new UnpublishArticle($articleEditor->getDecoratedObject()))(); } - - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications( - 'com.woltlab.wcf.article.notification', - $articleIDs - ); - - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents( - 'com.woltlab.wcf.article.recentActivityEvent', - $articleIDs - ); - - ArticleEditor::updateArticleCounter($usersToArticles); - - $this->unmarkItems(); } /** @@ -954,26 +838,4 @@ public function search() return $articles; } - - /** - * Unmarks articles. - * - * @param int[] $articleIDs - * @return void - */ - protected function unmarkItems(array $articleIDs = []) - { - if (empty($articleIDs)) { - foreach ($this->getObjects() as $article) { - $articleIDs[] = $article->articleID; - } - } - - if (!empty($articleIDs)) { - ClipboardHandler::getInstance()->unmark( - $articleIDs, - ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article') - ); - } - } } diff --git a/wcfsetup/install/files/lib/event/article/ArticleCategorySet.class.php b/wcfsetup/install/files/lib/event/article/ArticleCategorySet.class.php new file mode 100644 index 00000000000..c2fa9997073 --- /dev/null +++ b/wcfsetup/install/files/lib/event/article/ArticleCategorySet.class.php @@ -0,0 +1,25 @@ + + * @since 6.3 + */ +final class ArticleCategorySet implements IPsr14Event +{ + public function __construct( + public readonly Article $article, + public readonly ArticleCategory $oldCategory, + public readonly ArticleCategory $newCategory, + ) { + } +} diff --git a/wcfsetup/install/files/lib/event/article/ArticlePublished.class.php b/wcfsetup/install/files/lib/event/article/ArticlePublished.class.php new file mode 100644 index 00000000000..2a2066951eb --- /dev/null +++ b/wcfsetup/install/files/lib/event/article/ArticlePublished.class.php @@ -0,0 +1,21 @@ + + * @since 6.3 + */ +final class ArticlePublished implements IPsr14Event +{ + public function __construct(public readonly Article $article) + { + } +} diff --git a/wcfsetup/install/files/lib/event/article/ArticleRestored.class.php b/wcfsetup/install/files/lib/event/article/ArticleRestored.class.php new file mode 100644 index 00000000000..31d9e7839bf --- /dev/null +++ b/wcfsetup/install/files/lib/event/article/ArticleRestored.class.php @@ -0,0 +1,21 @@ + + * @since 6.3 + */ +final class ArticleRestored implements IPsr14Event +{ + public function __construct(public readonly Article $article) + { + } +} diff --git a/wcfsetup/install/files/lib/event/article/ArticleSoftDeleted.class.php b/wcfsetup/install/files/lib/event/article/ArticleSoftDeleted.class.php new file mode 100644 index 00000000000..42b3763f6ae --- /dev/null +++ b/wcfsetup/install/files/lib/event/article/ArticleSoftDeleted.class.php @@ -0,0 +1,21 @@ + + * @since 6.3 + */ +final class ArticleSoftDeleted implements IPsr14Event +{ + public function __construct(public readonly Article $article) + { + } +} diff --git a/wcfsetup/install/files/lib/event/article/ArticleUnpublished.class.php b/wcfsetup/install/files/lib/event/article/ArticleUnpublished.class.php new file mode 100644 index 00000000000..d1de292de17 --- /dev/null +++ b/wcfsetup/install/files/lib/event/article/ArticleUnpublished.class.php @@ -0,0 +1,21 @@ + + * @since 6.3 + */ +final class ArticleUnpublished implements IPsr14Event +{ + public function __construct(public readonly Article $article) + { + } +} diff --git a/wcfsetup/install/files/lib/page/AbstractArticlePage.class.php b/wcfsetup/install/files/lib/page/AbstractArticlePage.class.php index ba4deb43876..302155a6532 100644 --- a/wcfsetup/install/files/lib/page/AbstractArticlePage.class.php +++ b/wcfsetup/install/files/lib/page/AbstractArticlePage.class.php @@ -2,7 +2,7 @@ namespace wcf\page; -use wcf\data\article\ArticleAction; +use wcf\command\article\MarkArticleAsRead; use wcf\data\article\ArticleEditor; use wcf\data\article\category\ArticleCategory; use wcf\data\article\content\ViewableArticleContent; @@ -128,10 +128,7 @@ public function readData() // update article visit if ($this->article->isNew()) { - $articleAction = new ArticleAction([$this->article->getDecoratedObject()], 'markAsRead', [ - 'viewableArticle' => $this->article, - ]); - $articleAction->executeAction(); + (new MarkArticleAsRead($this->article->getDecoratedObject()))(); } // get tags diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/MarkAllArticlesAsRead.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/MarkAllArticlesAsRead.class.php new file mode 100644 index 00000000000..b75a839889c --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/MarkAllArticlesAsRead.class.php @@ -0,0 +1,45 @@ + + * @since 6.3 + */ +#[PostRequest('/core/articles/mark-all-as-read')] +final class MarkAllArticlesAsRead implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + if (!MODULE_ARTICLE) { + throw new IllegalLinkException(); + } + + $this->assertUserIsLoggedIn(); + + (new \wcf\command\article\MarkAllArticlesAsRead())(); + + return new JsonResponse([]); + } + + private function assertUserIsLoggedIn(): void + { + if (!WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/PublishArticle.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/PublishArticle.class.php index cb0fbf61e23..59b21c9bf57 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/PublishArticle.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/PublishArticle.class.php @@ -6,7 +6,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use wcf\data\article\Article; -use wcf\data\article\ArticleAction; use wcf\http\Helper; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; @@ -35,12 +34,10 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res if (!$article->canPublish()) { throw new PermissionDeniedException(); } - if ($article->publicationStatus === Article::PUBLISHED) { - throw new IllegalLinkException(); - } - $action = new ArticleAction([$article], 'publish'); - $action->executeAction(); + if ($article->publicationStatus !== Article::PUBLISHED) { + (new \wcf\command\article\PublishArticle($article))(); + } return new JsonResponse([]); } diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/RestoreArticle.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/RestoreArticle.class.php index 2cfb08ad6c1..aab276ed338 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/RestoreArticle.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/RestoreArticle.class.php @@ -6,7 +6,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use wcf\data\article\Article; -use wcf\data\article\ArticleAction; use wcf\http\Helper; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; @@ -39,8 +38,7 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res throw new IllegalLinkException(); } - $action = new ArticleAction([$article], 'restore'); - $action->executeAction(); + (new \wcf\command\article\RestoreArticle($article))(); return new JsonResponse([]); } diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/SoftDeleteArticle.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/SoftDeleteArticle.class.php index 343cacb04a7..edfa241d992 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/SoftDeleteArticle.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/SoftDeleteArticle.class.php @@ -6,7 +6,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use wcf\data\article\Article; -use wcf\data\article\ArticleAction; use wcf\http\Helper; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; @@ -39,8 +38,7 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res throw new IllegalLinkException(); } - $action = new ArticleAction([$article], 'trash'); - $action->executeAction(); + (new \wcf\command\article\SoftDeleteArticle($article))(); return new JsonResponse([]); } diff --git a/wcfsetup/install/files/lib/system/user/notification/event/ArticleLikeUserNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/ArticleLikeUserNotificationEvent.class.php index bc144ceae0a..ca3149aefe4 100644 --- a/wcfsetup/install/files/lib/system/user/notification/event/ArticleLikeUserNotificationEvent.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/event/ArticleLikeUserNotificationEvent.class.php @@ -2,13 +2,12 @@ namespace wcf\system\user\notification\event; +use wcf\command\article\ResetUserStorageForUnreadArticles; use wcf\data\article\category\ArticleCategory; use wcf\data\article\LikeableArticle; use wcf\data\user\UserProfile; use wcf\system\cache\runtime\ViewableArticleRuntimeCache; use wcf\system\user\notification\object\LikeUserNotificationObject; -use wcf\system\user\storage\UserStorageHandler; -use wcf\system\WCF; /** * User notification event for post likes. @@ -128,10 +127,7 @@ public function getEventHash() public function checkAccess() { if (!ViewableArticleRuntimeCache::getInstance()->getObject($this->getUserNotificationObject()->objectID)->canRead()) { - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'wbbUnreadWatchedThreads'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); + (new ResetUserStorageForUnreadArticles())(); return false; }