From fb73351072dc762227c16248cc90f5f488689ffd Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 11:13:51 +0100 Subject: [PATCH] Add new endpoint of render the full quote of a message --- .../Core/Api/Messages/RenderQuote.ts | 29 +++++++ ts/WoltLabSuite/Core/Component/Quote/List.ts | 9 +- .../Core/Component/Quote/Message.ts | 55 +++++++++--- .../Core/Component/Quote/Storage.ts | 85 +++++++++++++++++++ ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 2 +- .../WoltLabSuite/Core/Component/Quote/List.js | 14 +-- .../Core/Component/Quote/Message.js | 39 ++++++--- .../js/WoltLabSuite/Core/Ui/Message/Quote.js | 2 +- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../core/messages/RenderQuote.class.php | 63 ++++++++++++++ 10 files changed, 264 insertions(+), 35 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts create mode 100644 ts/WoltLabSuite/Core/Component/Quote/Storage.ts create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts new file mode 100644 index 0000000000..8f389ff930 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -0,0 +1,29 @@ +/** + * Requests render a full quote of a message. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; + +type Response = string; + +export async function renderQuote(objectType: string, objectID: number): Promise> { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/renderquote"); + url.searchParams.set("objectType", objectType); + url.searchParams.set("objectID", objectID.toString()); + + let response: Response; + try { + response = (await prepareRequest(url).get().fetchAsJson()) as Response; + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue(response); +} diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 0768878da3..b047baacf7 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -8,14 +8,13 @@ * @woltlabExcludeBundle tiny */ -import * as Core from "WoltLabSuite/Core/Core"; import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; +import { getQuotes } from "WoltLabSuite/Core/Component/Quote/Storage"; -export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const quoteLists = new Map(); class QuoteList { @@ -39,7 +38,11 @@ class QuoteList { } public renderQuotes(): void { - this.#container.innerHTML = window.localStorage.getItem(STORAGE_KEY) || ""; + this.#container.innerHTML = ""; + + for (const [, quotes] of getQuotes()) { + // TODO render quotes + } if (this.#container.hasChildNodes()) { getTabMenu(this.#editorId)?.showTab( diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index a8c2665057..b0297ab22b 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -13,27 +13,38 @@ import { getPhrase } from "WoltLabSuite/Core/Language"; import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment"; import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; +import { saveQuote, saveFullQuote } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; interface Container { element: HTMLElement; messageBodySelector: string; + objectType: string; + objectId: number; } +let selectedMessage: + | undefined + | { + message: string; + container: Container; + }; + const containers = new Map(); let activeMessageId = ""; -let message = ""; let activeEditor: CKEditor | undefined = undefined; let timerSelectionChange: number | undefined = undefined; let isMouseDown = false; -let objectId: number | undefined = undefined; const copyQuote = document.createElement("div"); -export function registerContainer(containerSelector: string, messageBodySelector: string): void { +export function registerContainer(containerSelector: string, messageBodySelector: string, objectType: string): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); containers.set(id, { element: container, messageBodySelector: messageBodySelector, + objectType: objectType, + objectId: ~~container.dataset.objectId!, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -43,13 +54,19 @@ export function registerContainer(containerSelector: string, messageBodySelector container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); - container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => { - //TODO - }); + container.querySelector(".jsQuoteMessage")?.addEventListener( + "click", + promiseMutex(async (event: MouseEvent) => { + event.preventDefault(); + + await saveFullQuote(objectType, ~~container.dataset.objectId!); + //TODO insert into `activeEditor` + }), + ); }); } -export function setActiveEditor(editor: CKEditor, supportDirectInsert: boolean) { +export function setActiveEditor(editor?: CKEditor, supportDirectInsert: boolean = false) { copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert")!.hidden = !supportDirectInsert; activeEditor = editor; @@ -63,7 +80,9 @@ function setup() { buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = getPhrase("wcf.message.quote.quoteSelected"); buttonSaveQuote.addEventListener("click", () => { - //TODO + saveQuote(selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.message); + + removeSelection(); }); copyQuote.appendChild(buttonSaveQuote); const buttonSaveAndInsertQuote = document.createElement("button"); @@ -72,7 +91,10 @@ function setup() { buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = getPhrase("wcf.message.quote.quoteAndReply"); buttonSaveAndInsertQuote.addEventListener("click", () => { - //TODO + saveQuote(selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.message); + //TODO insert into `activeEditor` + + removeSelection(); }); copyQuote.appendChild(buttonSaveAndInsertQuote); @@ -386,8 +408,19 @@ function onMouseUp(event?: MouseEvent): void { const text = getSelectedText().trim(); if (text !== "") { copyQuote.classList.add("active"); - message = text; - objectId = ~~container.element.dataset.objectId!; + selectedMessage = { + message: text, + container: container, + }; } }, 10); } + +function removeSelection(): void { + copyQuote.classList.remove("active"); + + const selection = window.getSelection()!; + if (selection.rangeCount) { + selection.removeAllRanges(); + } +} diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts new file mode 100644 index 0000000000..61bd25b830 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -0,0 +1,85 @@ +/** + * Stores the quote data. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import * as Core from "WoltLabSuite/Core/Core"; +import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; + +interface StorageData { + quotes: Map>; +} + +export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; + +export function saveQuote(objectType: string, objectId: number, message: string) { + const storage = getStorage(); + + const key = getKey(objectType, objectId); + if (!storage.quotes.has(key)) { + storage.quotes.set(key, new Set()); + } + + storage.quotes.get(key)!.add(message); + + saveStorage(storage); +} + +export async function saveFullQuote(objectType: string, objectId: number) { + const result = await renderQuote(objectType, objectId); + if (!result.ok) { + // TODO error handling + return; + } + + saveQuote(objectType, objectId, result.value); +} + +export function getQuotes(): Map> { + return getStorage().quotes; +} + +function getStorage(): StorageData { + const data = window.localStorage.getItem(STORAGE_KEY); + if (data === null) { + return { + quotes: new Map(), + }; + } else { + return JSON.parse(data, (key, value) => { + if (key === "quotes") { + const result = new Map>(value); + for (const [key, setValue] of result) { + result.set(key, new Set(setValue)); + } + return result; + } + + return value; + }); + } +} + +function getKey(objectType: string, objectId: number): string { + return `${objectType}:${objectId}`; +} + +function saveStorage(data: StorageData) { + window.localStorage.setItem( + STORAGE_KEY, + JSON.stringify(data, (key, value) => { + if (value instanceof Map) { + return Array.from(value.entries()); + } else if (value instanceof Set) { + return Array.from(value); + } + + return value; + }), + ); +} diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index 8a2ee18458..29b39a52d1 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -25,7 +25,7 @@ export class UiMessageQuote { messageContentSelector: string, supportDirectInsert: boolean, ) { - registerContainer(containerSelector, messageBodySelector); + registerContainer(containerSelector, messageBodySelector, objectType); } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js index b3ff4a52a5..a7a080d342 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -7,14 +7,11 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1, Message_1) { +define(["require", "exports", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage"], function (require, exports, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.STORAGE_KEY = void 0; exports.getQuoteList = getQuoteList; exports.setup = setup; - Core = tslib_1.__importStar(Core); - exports.STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const quoteLists = new Map(); class QuoteList { #container; @@ -33,7 +30,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C this.renderQuotes(); } renderQuotes() { - this.#container.innerHTML = window.localStorage.getItem(exports.STORAGE_KEY) || ""; + this.#container.innerHTML = ""; + for (const [, quotes] of (0, Storage_1.getQuotes)()) { + // TODO render quotes + } if (this.#container.hasChildNodes()) { (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { count: this.#container.childElementCount, @@ -60,8 +60,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); } (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); - ckeditor.focusTracker.on("change:isFocused", () => { - if (ckeditor.focusTracker.isFocused) { + ckeditor.focusTracker.on("change:isFocused", (_evt, _name, isFocused) => { + if (isFocused) { (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); } }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js index 3068f6aad6..e213a4cf2f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -7,38 +7,41 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1, Storage_1, PromiseMutex_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerContainer = registerContainer; exports.setActiveEditor = setActiveEditor; Util_1 = tslib_1.__importDefault(Util_1); + let selectedMessage; const containers = new Map(); let activeMessageId = ""; - let message = ""; let activeEditor = undefined; let timerSelectionChange = undefined; let isMouseDown = false; - let objectId = undefined; const copyQuote = document.createElement("div"); - function registerContainer(containerSelector, messageBodySelector) { + function registerContainer(containerSelector, messageBodySelector, objectType) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); containers.set(id, { element: container, messageBodySelector: messageBodySelector, + objectType: objectType, + objectId: ~~container.dataset.objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { return; } container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); - container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => { - //TODO - }); + container.querySelector(".jsQuoteMessage")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { + event.preventDefault(); + await (0, Storage_1.saveFullQuote)(objectType, ~~container.dataset.objectId); + //TODO insert into `activeEditor` + })); }); } - function setActiveEditor(editor, supportDirectInsert) { + function setActiveEditor(editor, supportDirectInsert = false) { copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert").hidden = !supportDirectInsert; activeEditor = editor; } @@ -49,7 +52,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteSelected"); buttonSaveQuote.addEventListener("click", () => { - //TODO + (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + removeSelection(); }); copyQuote.appendChild(buttonSaveQuote); const buttonSaveAndInsertQuote = document.createElement("button"); @@ -58,7 +62,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteAndReply"); buttonSaveAndInsertQuote.addEventListener("click", () => { - //TODO + (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + //TODO insert into `activeEditor` + removeSelection(); }); copyQuote.appendChild(buttonSaveAndInsertQuote); document.body.appendChild(copyQuote); @@ -303,9 +309,18 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui const text = getSelectedText().trim(); if (text !== "") { copyQuote.classList.add("active"); - message = text; - objectId = ~~container.element.dataset.objectId; + selectedMessage = { + message: text, + container: container, + }; } }, 10); } + function removeSelection() { + copyQuote.classList.remove("active"); + const selection = window.getSelection(); + if (selection.rangeCount) { + selection.removeAllRanges(); + } + } }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js index fa5784e730..9399bb00c2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -12,7 +12,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func * Initializes the quote handler for given object type. */ constructor(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) { - (0, Message_1.registerContainer)(containerSelector, messageBodySelector); + (0, Message_1.registerContainer)(containerSelector, messageBodySelector, objectType); } } exports.UiMessageQuote = UiMessageQuote; diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 5c673485c3..c937638498 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -137,6 +137,7 @@ return static function (): void { $event->register(new \wcf\system\endpoint\controller\core\comments\responses\UpdateResponse()); $event->register(new \wcf\system\endpoint\controller\core\cronjobs\logs\ClearLogs()); $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions()); + $event->register(new \wcf\system\endpoint\controller\core\messages\RenderQuote()); $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession()); $event->register(new \wcf\system\endpoint\controller\core\versionTrackers\RevertVersion()); $event->register(new \wcf\system\endpoint\controller\core\moderationQueues\ChangeJustifiedStatus()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php new file mode 100644 index 0000000000..76d6ba53cd --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php @@ -0,0 +1,63 @@ + + * @since 6.2 + */ +#[GetRequest('/core/messages/renderquote')] +final class RenderQuote implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $parameters = Helper::mapApiParameters($request, GetRenderQuoteParameters::class); + + return new JsonResponse( + $this->renderFullQuote($parameters), + 200, + ); + } + + private function renderFullQuote(GetRenderQuoteParameters $parameters): string + { + // TODO load object + /** @var $object IMessage */ + // TODO load embedded objects? + + $htmlInputProcessor = new HtmlInputProcessor(); + $htmlInputProcessor->processIntermediate($object->getMessage()); + + if (MESSAGE_MAX_QUOTE_DEPTH) { + $htmlInputProcessor->enforceQuoteDepth(MESSAGE_MAX_QUOTE_DEPTH - 1, true); + } + + return $htmlInputProcessor->getHtml(); + } +} + +/** @internal */ +final class GetRenderQuoteParameters +{ + public function __construct( + /** @var non-empty-string */ + public readonly string $objectType, + /** @var positive-int */ + public readonly int $objectID, + ) { + } +} -- 2.20.1