From 6906b13989f30f05855f807000ec1bd0f9c352f6 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 2 Aug 2025 11:40:44 +0200 Subject: [PATCH 01/10] (Ab)use the legacy API to retrieve data for quotes --- .../Core/Component/Quote/Message.ts | 38 ++++++---- .../Core/Component/Quote/Storage.ts | 18 +++++ ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 4 +- .../Core/Component/Quote/Message.js | 15 ++-- .../Core/Component/Quote/Storage.js | 13 +++- .../js/WoltLabSuite/Core/Ui/Message/Quote.js | 6 +- .../AbstractMessageQuoteHandler.class.php | 10 +++ .../quote/MessageQuoteManager.class.php | 72 ++++++++++++++++++- 8 files changed, 148 insertions(+), 28 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 9f6ce5283f7..45ffbcfda0d 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -20,17 +20,18 @@ import { isFullQuoted, getKey, removeQuotes, + legacySaveQuote, } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; -interface Container { +type Container = { element: HTMLElement; messageBodySelector: string; objectType: string; className: string; objectId: number; -} +}; let selectedMessage: | undefined @@ -39,12 +40,12 @@ let selectedMessage: container: Container; }; -interface ElementBoundaries { +type ElementBoundaries = { bottom: number; left: number; right: number; top: number; -} +}; const containers = new Map(); const quoteMessageButtons = new Map(); @@ -66,10 +67,10 @@ export function registerContainer( containers.set(id, { element: container, - messageBodySelector: messageBodySelector, - objectType: objectType, - className: className, - objectId: objectId, + messageBodySelector, + objectType, + className, + objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -152,12 +153,21 @@ function setup() { buttonSaveQuote.addEventListener( "click", promiseMutex(async () => { - await saveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.container.className, - selectedMessage!.message, - ); + if (selectedMessage!.container.className.endsWith("Action")) { + await legacySaveQuote( + selectedMessage!.container.objectType, + selectedMessage!.container.objectId, + selectedMessage!.container.className, + selectedMessage!.message, + ); + } else { + await saveQuote( + selectedMessage!.container.objectType, + selectedMessage!.container.objectId, + selectedMessage!.container.className, + selectedMessage!.message, + ); + } removeSelection(); }), diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index ba1b9f0d681..fb1a153983f 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -14,6 +14,7 @@ import { getMessageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; import { refreshQuoteLists } from "WoltLabSuite/Core/Component/Quote/List"; import { resetRemovalQuotes } from "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes"; import { removeQuoteStatus } from "WoltLabSuite/Core/Component/Quote/Message"; +import { dboAction } from "WoltLabSuite/Core/Ajax"; interface Message { objectID: number; @@ -38,6 +39,23 @@ interface StorageData { const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map>(); +export async function legacySaveQuote( + objectType: string, + objectId: number, + className: string, + message: string, +): Promise { + const result = await dboAction("saveQuote", className) + .objectIds([objectId]) + .payload({ + message, + renderQuote: true, + }) + .dispatch(); + + debugger; +} + export async function saveQuote( objectType: string, objectId: number, diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index a3dad998ce7..2137e3e821b 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -26,9 +26,9 @@ export class UiMessageQuote { _supportDirectInsert: boolean, ) { // remove "Action" from className - if (className.endsWith("Action")) { + /*if (className.endsWith("Action")) { className = className.substring(0, className.length - 6); - } + }*/ registerContainer(containerSelector, messageBodySelector, className, objectType); } 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 84db64b7535..1c9a19893be 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -29,10 +29,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui const objectId = ~~container.dataset.objectId; containers.set(id, { element: container, - messageBodySelector: messageBodySelector, - objectType: objectType, - className: className, - objectId: objectId, + messageBodySelector, + objectType, + className, + objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { return; @@ -93,7 +93,12 @@ 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", (0, PromiseMutex_1.promiseMutex)(async () => { - await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + if (selectedMessage.container.className.endsWith("Action")) { + await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + } + else { + await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + } removeSelection(); })); copyQuote.appendChild(buttonSaveQuote); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index aa394d70483..ff9fbd5cc15 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -7,9 +7,10 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Api/Messages/Author", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_1, Message_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Api/Messages/Author", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Ajax"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_1, Message_1, Ajax_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); + exports.legacySaveQuote = legacySaveQuote; exports.saveQuote = saveQuote; exports.saveFullQuote = saveFullQuote; exports.getQuotes = getQuotes; @@ -25,6 +26,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C Core = tslib_1.__importStar(Core); const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map(); + async function legacySaveQuote(objectType, objectId, className, message) { + const result = await (0, Ajax_1.dboAction)("saveQuote", className) + .objectIds([objectId]) + .payload({ + message, + renderQuote: true, + }) + .dispatch(); + debugger; + } async function saveQuote(objectType, objectId, objectClassName, message) { const result = await (0, Author_1.getMessageAuthor)(objectClassName, objectId); if (!result.ok) { 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 0d65a51114d..9cfb8620455 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -13,9 +13,9 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func */ constructor(_quoteManager, className, objectType, containerSelector, messageBodySelector, _messageContentSelector, _supportDirectInsert) { // remove "Action" from className - if (className.endsWith("Action")) { - className = className.substring(0, className.length - 6); - } + /*if (className.endsWith("Action")) { + className = className.substring(0, className.length - 6); + }*/ (0, Message_1.registerContainer)(containerSelector, messageBodySelector, className, objectType); } } diff --git a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php index 05b5cc78299..56301d60597 100644 --- a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php @@ -112,4 +112,14 @@ protected function overrideIsFullQuote(array $messages) * @return QuotedMessage[] */ abstract protected function getMessages(array $data); + + /** + * @return list + */ + public function legacyGetMessages(int $objectID, string $marker): array + { + return $this->getMessages([ + $objectID => [$marker], + ]); + } } diff --git a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php index 0e9d21a9aca..20889a26ee0 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -3,6 +3,9 @@ namespace wcf\system\message\quote; use wcf\data\IMessage; +use wcf\data\object\type\ObjectTypeCache; +use wcf\data\user\avatar\DefaultAvatar; +use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\event\EventHandler; use wcf\system\SingletonFactory; use wcf\system\WCF; @@ -15,7 +18,7 @@ * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License */ -class MessageQuoteManager extends SingletonFactory +final class MessageQuoteManager extends SingletonFactory { /** * list of quote ids to be removed @@ -30,6 +33,19 @@ class MessageQuoteManager extends SingletonFactory */ protected array $usedQuotes = []; + /** + * @var array{ + * objectType: string, + * parentObjectID: int, + * objectID: int, + * message: string, + * fullQuote: string, + * } + */ + private array $legacyQuoteData; + + private const LEGACY_QUOTE_MARKER = '@@@legacy_quote@@@'; + /** * @inheritDoc */ @@ -65,7 +81,19 @@ public function addQuote( $fullQuote = '', $returnFalseIfExists = true ) { - return false; + if (isset($this->legacyQuoteData)) { + throw new \RuntimeException("Cannot store another quote, there is already one legacy quote present."); + } + + $this->legacyQuoteData = [ + 'objectType' => $objectType, + 'parentObjectID' => $parentObjectID, + 'objectID' => $objectID, + 'message' => $message, + 'fullQuote' => $fullQuote, + ]; + + return self::LEGACY_QUOTE_MARKER; } /** @@ -107,7 +135,45 @@ public function removeQuote($quoteID) */ public function getQuoteComponents($quoteID) { - return false; + if ($quoteID !== self::LEGACY_QUOTE_MARKER) { + throw new \RuntimeException("Encountered an unexpected quote id, found '{$quoteID}'."); + } + + \assert(isset($this->legacyQuoteData)); + + $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message.quote', $this->legacyQuoteData['objectType']); + if ($objectType === null) { + throw new \RuntimeException("Cannot find the object type '{$this->legacyQuoteData['objectType']}' for quotes."); + } + + $quoteHandler = \call_user_func([$objectType->className, 'getInstance']); + \assert($quoteHandler instanceof AbstractMessageQuoteHandler); + + $messages = $quoteHandler->legacyGetMessages($this->legacyQuoteData['objectID'], self::LEGACY_QUOTE_MARKER); + $message = \current($messages); + \assert($message !== false); + + $avatar = ''; + if ($message->getUserID()) { + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($message->getUserID()); + if ($userProfile !== null) { + $avatar = $userProfile->getAvatar()->getURL(); + } + } + + if ($avatar === '') { + $avatar = (new DefaultAvatar($message->getUsername()))->getURL(); + } + + return [ + 'objectID' => $this->legacyQuoteData['objectID'], + 'authorID' => $message->getUserID(), + 'author' => $message->getUsername(), + 'title' => $message->getTitle(), + 'avatar' => $avatar, + 'time' => (new \DateTime('@' . $message->getTime()))->format("c"), + 'link' => $message->getLink(), + ]; } /** From ef8d5c76e10cc7186e4b732c5dd5e0e2a4f59057 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 2 Aug 2025 12:45:31 +0200 Subject: [PATCH 02/10] Add support for legacy full quotes --- .../Core/Component/Quote/Message.ts | 32 +++++++--- .../Core/Component/Quote/Storage.ts | 61 +++++++++++++++++-- .../Core/Component/Quote/Message.js | 18 +++++- .../Core/Component/Quote/Storage.js | 42 ++++++++++++- .../quote/MessageQuoteManager.class.php | 32 +++++++++- 5 files changed, 168 insertions(+), 17 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 45ffbcfda0d..b5f800943d2 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -21,6 +21,7 @@ import { getKey, removeQuotes, legacySaveQuote, + legacySaveFullQuote, } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; @@ -105,7 +106,13 @@ export function registerContainer( return; } - const quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!); + // TODO + let quoteMessage: any; + if (className.endsWith("Action")) { + quoteMessage = await legacySaveFullQuote(objectType, className, ~~container.dataset.objectId!); + } else { + quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!); + } quoteMessageButton!.classList.add("active"); if (activeEditor !== undefined) { @@ -181,12 +188,23 @@ function setup() { buttonSaveAndInsertQuote.addEventListener( "click", promiseMutex(async () => { - const quoteMessage = await saveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.container.className, - selectedMessage!.message, - ); + // TODO + let quoteMessage: any; + if (selectedMessage!.container.className.endsWith("Action")) { + quoteMessage = await legacySaveQuote( + selectedMessage!.container.objectType, + selectedMessage!.container.objectId, + selectedMessage!.container.className, + selectedMessage!.message, + ); + } else { + quoteMessage = await saveQuote( + selectedMessage!.container.objectType, + selectedMessage!.container.objectId, + selectedMessage!.container.className, + selectedMessage!.message, + ); + } if (activeEditor !== undefined) { dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index fb1a153983f..e7949e7aa4f 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -36,6 +36,12 @@ interface StorageData { messages: Map; } +type LegacyQuoteData = { + count: number; + fullQuoteMessageIDs: number[]; + renderedQuote: Message; +}; + const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map>(); @@ -44,16 +50,26 @@ export async function legacySaveQuote( objectId: number, className: string, message: string, -): Promise { - const result = await dboAction("saveQuote", className) +): Promise { + const result = (await dboAction("saveQuote", className) .objectIds([objectId]) .payload({ message, renderQuote: true, }) - .dispatch(); + .dispatch()) as LegacyQuoteData; + + const uuid = storeQuote(objectType, result.renderedQuote, { + message, + }); - debugger; + refreshQuoteLists(); + + return { + ...result.renderedQuote, + message, + uuid, + }; } export async function saveQuote( @@ -80,6 +96,43 @@ export async function saveQuote( }; } +export async function legacySaveFullQuote( + objectType: string, + objectClassName: string, + objectId: number, +): Promise { + const result = (await dboAction("saveFullQuote", objectClassName) + .objectIds([objectId]) + .dispatch()) as LegacyQuoteData; + + const message = { + objectID: result.renderedQuote.objectID, + time: result.renderedQuote.time, + title: result.renderedQuote.title, + link: result.renderedQuote.link, + authorID: result.renderedQuote.authorID, + author: result.renderedQuote.author, + avatar: result.renderedQuote.avatar, + }; + + const quote = { + // TODO + message: (result.renderedQuote as any).message, + // TODO + rawMessage: (result.renderedQuote as any).rawMessage, + }; + + const uuid = storeQuote(objectType, message, quote); + + refreshQuoteLists(); + + return { + ...message, + ...quote, + uuid, + }; +} + export async function saveFullQuote( objectType: string, objectClassName: string, 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 1c9a19893be..849313cf20d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -57,7 +57,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui quoteMessageButton.classList.remove("active"); return; } - const quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); + // TODO + let quoteMessage; + if (className.endsWith("Action")) { + quoteMessage = await (0, Storage_1.legacySaveFullQuote)(objectType, className, ~~container.dataset.objectId); + } + else { + quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); + } quoteMessageButton.classList.add("active"); if (activeEditor !== undefined) { (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ @@ -108,7 +115,14 @@ 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", (0, PromiseMutex_1.promiseMutex)(async () => { - const quoteMessage = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + // TODO + let quoteMessage; + if (selectedMessage.container.className.endsWith("Action")) { + quoteMessage = await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + } + else { + quoteMessage = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + } if (activeEditor !== undefined) { (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index ff9fbd5cc15..5c2f7c5c8e6 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -12,6 +12,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C Object.defineProperty(exports, "__esModule", { value: true }); exports.legacySaveQuote = legacySaveQuote; exports.saveQuote = saveQuote; + exports.legacySaveFullQuote = legacySaveFullQuote; exports.saveFullQuote = saveFullQuote; exports.getQuotes = getQuotes; exports.getMessage = getMessage; @@ -27,14 +28,22 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map(); async function legacySaveQuote(objectType, objectId, className, message) { - const result = await (0, Ajax_1.dboAction)("saveQuote", className) + const result = (await (0, Ajax_1.dboAction)("saveQuote", className) .objectIds([objectId]) .payload({ message, renderQuote: true, }) - .dispatch(); - debugger; + .dispatch()); + const uuid = storeQuote(objectType, result.renderedQuote, { + message, + }); + (0, List_1.refreshQuoteLists)(); + return { + ...result.renderedQuote, + message, + uuid, + }; } async function saveQuote(objectType, objectId, objectClassName, message) { const result = await (0, Author_1.getMessageAuthor)(objectClassName, objectId); @@ -51,6 +60,33 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C uuid, }; } + async function legacySaveFullQuote(objectType, objectClassName, objectId) { + const result = (await (0, Ajax_1.dboAction)("saveFullQuote", objectClassName) + .objectIds([objectId]) + .dispatch()); + const message = { + objectID: result.renderedQuote.objectID, + time: result.renderedQuote.time, + title: result.renderedQuote.title, + link: result.renderedQuote.link, + authorID: result.renderedQuote.authorID, + author: result.renderedQuote.author, + avatar: result.renderedQuote.avatar, + }; + const quote = { + // TODO + message: result.renderedQuote.message, + // TODO + rawMessage: result.renderedQuote.rawMessage, + }; + const uuid = storeQuote(objectType, message, quote); + (0, List_1.refreshQuoteLists)(); + return { + ...message, + ...quote, + uuid, + }; + } async function saveFullQuote(objectType, objectClassName, objectId) { const result = await (0, RenderQuote_1.renderQuote)(objectType, objectClassName, objectId); if (!result.ok) { diff --git a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php index 20889a26ee0..978e9c7660e 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -7,6 +7,7 @@ use wcf\data\user\avatar\DefaultAvatar; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\event\EventHandler; +use wcf\system\html\input\HtmlInputProcessor; use wcf\system\SingletonFactory; use wcf\system\WCF; use wcf\util\ArrayUtil; @@ -85,6 +86,20 @@ public function addQuote( throw new \RuntimeException("Cannot store another quote, there is already one legacy quote present."); } + if ($fullQuote !== '') { + $htmlInputProcessor = new HtmlInputProcessor(); + $htmlInputProcessor->processIntermediate($fullQuote); + + if (MESSAGE_MAX_QUOTE_DEPTH) { + $htmlInputProcessor->enforceQuoteDepth(MESSAGE_MAX_QUOTE_DEPTH - 1, true); + } + + $parameters = ['htmlInputProcessor' => $htmlInputProcessor]; + EventHandler::getInstance()->fireAction($this, 'addFullQuote', $parameters); + + $fullQuote = $htmlInputProcessor->getHtml(); + } + $this->legacyQuoteData = [ 'objectType' => $objectType, 'parentObjectID' => $parentObjectID, @@ -165,6 +180,10 @@ public function getQuoteComponents($quoteID) $avatar = (new DefaultAvatar($message->getUsername()))->getURL(); } + $messageData = $quoteHandler->renderQuotes([ + $this->legacyQuoteData['objectID'] => [self::LEGACY_QUOTE_MARKER], + ]); + return [ 'objectID' => $this->legacyQuoteData['objectID'], 'authorID' => $message->getUserID(), @@ -173,6 +192,8 @@ public function getQuoteComponents($quoteID) 'avatar' => $avatar, 'time' => (new \DateTime('@' . $message->getTime()))->format("c"), 'link' => $message->getLink(), + 'message' => $messageData[0], + 'rawMessage' => $this->isFullQuote(self::LEGACY_QUOTE_MARKER) ? $message->getMessage() : $this->legacyQuoteData['message'], ]; } @@ -234,6 +255,13 @@ public function getQuotesByParentObjectID($objectType, $parentObjectID, $markFor */ public function getQuote($quoteID, $useFullQuote = true) { + \assert(isset($this->legacyQuoteData)); + if ($useFullQuote && $this->legacyQuoteData['fullQuote'] !== '') { + return $this->legacyQuoteData['fullQuote']; + } else { + return $this->legacyQuoteData['message']; + } + return null; } @@ -422,7 +450,9 @@ public function removeOrphanedQuotes(array $quoteIDs) {} */ public function isFullQuote($quoteID) { - return false; + \assert(isset($this->legacyQuoteData)); + + return $this->legacyQuoteData['fullQuote'] !== ''; } /** From 94dedde63547408b3cfa963edd7966e86123ef55 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 2 Aug 2025 16:24:27 +0200 Subject: [PATCH 03/10] Reduce the amount of data required for quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These fields aren’t used anyway. --- ts/WoltLabSuite/Core/Api/Messages/Author.ts | 3 --- ts/WoltLabSuite/Core/Component/Quote/Storage.ts | 6 ------ .../files/js/WoltLabSuite/Core/Component/Quote/Storage.js | 3 --- .../controller/core/messages/GetMessageAuthor.class.php | 3 --- .../lib/system/message/quote/MessageQuoteManager.class.php | 3 --- 5 files changed, 18 deletions(-) diff --git a/ts/WoltLabSuite/Core/Api/Messages/Author.ts b/ts/WoltLabSuite/Core/Api/Messages/Author.ts index a2a08210dcf..fe88963011c 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/Author.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/Author.ts @@ -13,10 +13,7 @@ import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; type Response = { objectID: number; - authorID: number; author: string; - time: string; - title: string; link: string; avatar: string; }; diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index e7949e7aa4f..b88cd6042c8 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -18,10 +18,7 @@ import { dboAction } from "WoltLabSuite/Core/Ajax"; interface Message { objectID: number; - time: string; - title: string; link: string; - authorID: number | null; author: string; avatar: string; } @@ -107,10 +104,7 @@ export async function legacySaveFullQuote( const message = { objectID: result.renderedQuote.objectID, - time: result.renderedQuote.time, - title: result.renderedQuote.title, link: result.renderedQuote.link, - authorID: result.renderedQuote.authorID, author: result.renderedQuote.author, avatar: result.renderedQuote.avatar, }; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index 5c2f7c5c8e6..66f8c09833a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -66,10 +66,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C .dispatch()); const message = { objectID: result.renderedQuote.objectID, - time: result.renderedQuote.time, - title: result.renderedQuote.title, link: result.renderedQuote.link, - authorID: result.renderedQuote.authorID, author: result.renderedQuote.author, avatar: result.renderedQuote.avatar, }; diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php index ba531a7e9bd..d16228110f7 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php @@ -37,11 +37,8 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res return new JsonResponse( [ "objectID" => $object->getObjectID(), - "authorID" => $userProfile->getUserID(), "author" => $userProfile->getUsername(), - "title" => $object->getTitle(), "avatar" => $userProfile->getAvatar()->getURL(), - "time" => (new \DateTime('@' . $object->getTime()))->format("c"), "link" => $object->getLink(), ], 200, diff --git a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php index 978e9c7660e..b0030e31f4b 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -186,11 +186,8 @@ public function getQuoteComponents($quoteID) return [ 'objectID' => $this->legacyQuoteData['objectID'], - 'authorID' => $message->getUserID(), 'author' => $message->getUsername(), - 'title' => $message->getTitle(), 'avatar' => $avatar, - 'time' => (new \DateTime('@' . $message->getTime()))->format("c"), 'link' => $message->getLink(), 'message' => $messageData[0], 'rawMessage' => $this->isFullQuote(self::LEGACY_QUOTE_MARKER) ? $message->getMessage() : $this->legacyQuoteData['message'], From 08a709173c4a220687299946dd9c49beb98d5585 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 00:44:29 +0200 Subject: [PATCH 04/10] Use an event to collect the message being quoted --- .../Core/Api/Messages/RenderQuote.ts | 7 +-- .../Core/Component/Quote/Message.ts | 2 +- .../Core/Component/Quote/Storage.ts | 6 +-- .../Core/Api/Messages/RenderQuote.js | 5 +- .../Core/Component/Quote/Message.js | 2 +- .../Core/Component/Quote/Storage.js | 7 +-- .../message/MessageQuoteRendering.class.php | 37 ++++++++++++++ .../core/messages/RenderQuote.class.php | 50 ++++++++++--------- 8 files changed, 71 insertions(+), 45 deletions(-) create mode 100644 wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index f426872ea73..4b5108eafd9 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -13,11 +13,8 @@ import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; type Response = { objectID: number; - authorID: number | null; author: string; - time: string; link: string; - title: string; avatar: string; message: string | null; rawMessage: string | null; @@ -25,13 +22,11 @@ type Response = { export async function renderQuote( objectType: string, - className: string, objectID: number, ): Promise> { const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); url.searchParams.set("objectType", objectType); - url.searchParams.set("className", className); - url.searchParams.set("fullQuote", "true"); + url.searchParams.set("isFullQuote", "true"); url.searchParams.set("objectID", objectID.toString()); let response: Response; diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index b5f800943d2..766712ba43c 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -111,7 +111,7 @@ export function registerContainer( if (className.endsWith("Action")) { quoteMessage = await legacySaveFullQuote(objectType, className, ~~container.dataset.objectId!); } else { - quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!); + quoteMessage = await saveFullQuote(objectType, ~~container.dataset.objectId!); } quoteMessageButton!.classList.add("active"); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index b88cd6042c8..f0fd014d22a 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -129,20 +129,16 @@ export async function legacySaveFullQuote( export async function saveFullQuote( objectType: string, - objectClassName: string, objectId: number, ): Promise { - const result = await renderQuote(objectType, objectClassName, objectId); + const result = await renderQuote(objectType, objectId); if (!result.ok) { throw new Error("Error fetching quote data"); } const message = { objectID: result.value.objectID, - time: result.value.time, - title: result.value.title, link: result.value.link, - authorID: result.value.authorID, author: result.value.author, avatar: result.value.avatar, }; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js index 5d1c7fd4849..1175285930f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js @@ -11,11 +11,10 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderQuote = renderQuote; - async function renderQuote(objectType, className, objectID) { + async function renderQuote(objectType, objectID) { const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); url.searchParams.set("objectType", objectType); - url.searchParams.set("className", className); - url.searchParams.set("fullQuote", "true"); + url.searchParams.set("isFullQuote", "true"); url.searchParams.set("objectID", objectID.toString()); let response; try { 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 849313cf20d..346e8053a3d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -63,7 +63,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui quoteMessage = await (0, Storage_1.legacySaveFullQuote)(objectType, className, ~~container.dataset.objectId); } else { - quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); + quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, ~~container.dataset.objectId); } quoteMessageButton.classList.add("active"); if (activeEditor !== undefined) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index 66f8c09833a..e1bae83303c 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -84,17 +84,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C uuid, }; } - async function saveFullQuote(objectType, objectClassName, objectId) { - const result = await (0, RenderQuote_1.renderQuote)(objectType, objectClassName, objectId); + async function saveFullQuote(objectType, objectId) { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId); if (!result.ok) { throw new Error("Error fetching quote data"); } const message = { objectID: result.value.objectID, - time: result.value.time, - title: result.value.title, link: result.value.link, - authorID: result.value.authorID, author: result.value.author, avatar: result.value.avatar, }; diff --git a/wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php b/wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php new file mode 100644 index 00000000000..c37c7a3b9e9 --- /dev/null +++ b/wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php @@ -0,0 +1,37 @@ + + * @since 6.2 + */ +final class MessageQuoteRendering implements IPsr14Event +{ + private IMessage $message; + + public function __construct( + public readonly string $objectType, + public readonly int $objectID, + ) {} + + public function setMessage(IMessage $message): void + { + $this->message = $message; + } + + public function getMessage(): ?IMessage + { + return $this->message ?? null; + } +} 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 index 979e32d759e..0e24f2c432b 100644 --- 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 @@ -5,14 +5,16 @@ use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use wcf\data\DatabaseObject; use wcf\data\IEmbeddedMessageObject; use wcf\data\IMessage; use wcf\data\user\UserProfile; +use wcf\event\message\MessageQuoteRendering; use wcf\http\Helper; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\endpoint\GetRequest; use wcf\system\endpoint\IController; +use wcf\system\event\EventHandler; +use wcf\system\exception\PermissionDeniedException; use wcf\system\html\input\HtmlInputProcessor; /** @@ -31,33 +33,32 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetRenderQuoteParameters::class); - // @phpstan-ignore argument.templateType - $object = Helper::fetchObjectFromRequestParameter($parameters->objectID, $parameters->className); - \assert($object instanceof IMessage && $object instanceof DatabaseObject); + $event = new MessageQuoteRendering($parameters->objectType, $parameters->objectID); + EventHandler::getInstance()->fire($event); - $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); + $message = $event->getMessage(); + if ($message === null) { + throw new PermissionDeniedException(); + } + + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($message->getUserID()); if ($userProfile === null) { - $userProfile = UserProfile::getGuestUserProfile($object->getUsername()); + $userProfile = UserProfile::getGuestUserProfile($message->getUsername()); } - if ($object instanceof IEmbeddedMessageObject) { - $object->loadEmbeddedObjects(); + if ($message instanceof IEmbeddedMessageObject) { + $message->loadEmbeddedObjects(); } - return new JsonResponse( - [ - "objectID" => $object->getObjectID(), - "authorID" => $userProfile->getUserID(), - "author" => $userProfile->getUsername(), - "avatar" => $userProfile->getAvatar()->getURL(), - "time" => (new \DateTime('@' . $object->getTime()))->format("c"), - "title" => $object->getTitle(), - "link" => $object->getLink(), - "rawMessage" => $parameters->fullQuote ? $this->renderFullQuote($object) : null, - "message" => $parameters->fullQuote ? $object->getFormattedMessage() : null - ], - 200, - ); + return new JsonResponse([ + 'objectID' => $parameters->objectID, + 'authorID' => $userProfile->getUserID(), + 'author' => $userProfile->getUsername(), + 'avatar' => $userProfile->getAvatar()->getURL(), + 'link' => $message->getLink(), + 'rawMessage' => $parameters->isFullQuote ? $this->renderFullQuote($message) : null, + 'message' => $parameters->isFullQuote ? $message->getFormattedMessage() : null + ]); } private function renderFullQuote(IMessage $object): string @@ -78,9 +79,10 @@ final class GetRenderQuoteParameters { public function __construct( /** @var non-empty-string */ - public readonly string $className, + public readonly string $objectType, /** @var positive-int */ public readonly int $objectID, - public readonly bool $fullQuote = false, + + public readonly bool $isFullQuote, ) {} } From 95a2b25471c1923df0513a2c9d8d241a483451ef Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 00:53:19 +0200 Subject: [PATCH 05/10] Remove the `message-author` endpoint It can be completely replaced by the existing `render-quote` endpoint. --- ts/WoltLabSuite/Core/Api/Messages/Author.ts | 34 ---------- .../Core/Api/Messages/RenderQuote.ts | 3 +- ts/WoltLabSuite/Core/Component/Quote/List.ts | 4 +- .../Core/Component/Quote/Message.ts | 6 +- .../Core/Component/Quote/Storage.ts | 13 ++-- .../WoltLabSuite/Core/Api/Messages/Author.js | 27 -------- .../Core/Api/Messages/RenderQuote.js | 4 +- .../WoltLabSuite/Core/Component/Quote/List.js | 4 +- .../Core/Component/Quote/Message.js | 8 +-- .../Core/Component/Quote/Storage.js | 8 +-- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 - .../core/messages/GetMessageAuthor.class.php | 63 ------------------- 12 files changed, 22 insertions(+), 153 deletions(-) delete mode 100644 ts/WoltLabSuite/Core/Api/Messages/Author.ts delete mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js delete mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php diff --git a/ts/WoltLabSuite/Core/Api/Messages/Author.ts b/ts/WoltLabSuite/Core/Api/Messages/Author.ts deleted file mode 100644 index fe88963011c..00000000000 --- a/ts/WoltLabSuite/Core/Api/Messages/Author.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 = { - objectID: number; - author: string; - link: string; - avatar: string; -}; - -export async function getMessageAuthor(className: string, objectID: number): Promise> { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/message-author"); - url.searchParams.set("className", className); - url.searchParams.set("objectID", objectID.toString()); - - let response: Response; - try { - response = (await prepareRequest(url).get().allowCaching().fetchAsJson()) as Response; - } catch (e) { - return apiResultFromError(e); - } - - return apiResultFromValue(response); -} diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index 4b5108eafd9..449b0b67fab 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -23,10 +23,11 @@ type Response = { export async function renderQuote( objectType: string, objectID: number, + isFullQuote: boolean, ): Promise> { const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); url.searchParams.set("objectType", objectType); - url.searchParams.set("isFullQuote", "true"); + url.searchParams.set("isFullQuote", String(isFullQuote)); url.searchParams.set("objectID", objectID.toString()); let response: Response; diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 1b51acec89b..90f814a1efa 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -80,8 +80,8 @@ class QuoteList { dispatchToCkeditor(this.#editor).insertQuote({ author: message.author, - content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, - isText: quote.rawMessage === undefined, + content: quote.rawMessage ? quote.rawMessage : quote.message, + isText: !quote.rawMessage, link: message.link, }); }); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 766712ba43c..0d8c015edd8 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -171,7 +171,6 @@ function setup() { await saveQuote( selectedMessage!.container.objectType, selectedMessage!.container.objectId, - selectedMessage!.container.className, selectedMessage!.message, ); } @@ -201,7 +200,6 @@ function setup() { quoteMessage = await saveQuote( selectedMessage!.container.objectType, selectedMessage!.container.objectId, - selectedMessage!.container.className, selectedMessage!.message, ); } @@ -209,8 +207,8 @@ function setup() { if (activeEditor !== undefined) { dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, + content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, + isText: !quoteMessage.rawMessage, link: quoteMessage.link, }); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index f0fd014d22a..3b999b7269f 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -10,7 +10,6 @@ import * as Core from "WoltLabSuite/Core/Core"; import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; -import { getMessageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; import { refreshQuoteLists } from "WoltLabSuite/Core/Component/Quote/List"; import { resetRemovalQuotes } from "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes"; import { removeQuoteStatus } from "WoltLabSuite/Core/Component/Quote/Message"; @@ -25,7 +24,7 @@ interface Message { interface Quote { message: string; - rawMessage?: string; + rawMessage?: string | null; } interface StorageData { @@ -72,10 +71,9 @@ export async function legacySaveQuote( export async function saveQuote( objectType: string, objectId: number, - objectClassName: string, message: string, ): Promise { - const result = await getMessageAuthor(objectClassName, objectId); + const result = await renderQuote(objectType, objectId, false); if (!result.ok) { throw new Error("Error fetching author data"); } @@ -127,11 +125,8 @@ export async function legacySaveFullQuote( }; } -export async function saveFullQuote( - objectType: string, - objectId: number, -): Promise { - const result = await renderQuote(objectType, objectId); +export async function saveFullQuote(objectType: string, objectId: number): Promise { + const result = await renderQuote(objectType, objectId, true); if (!result.ok) { throw new Error("Error fetching quote data"); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js deleted file mode 100644 index be9deb91cb5..00000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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 - */ -define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getMessageAuthor = getMessageAuthor; - async function getMessageAuthor(className, objectID) { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/message-author"); - url.searchParams.set("className", className); - url.searchParams.set("objectID", objectID.toString()); - let response; - try { - response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().fetchAsJson()); - } - catch (e) { - return (0, Result_1.apiResultFromError)(e); - } - return (0, Result_1.apiResultFromValue)(response); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js index 1175285930f..08a5570ee62 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js @@ -11,10 +11,10 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderQuote = renderQuote; - async function renderQuote(objectType, objectID) { + async function renderQuote(objectType, objectID, isFullQuote) { const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); url.searchParams.set("objectType", objectType); - url.searchParams.set("isFullQuote", "true"); + url.searchParams.set("isFullQuote", String(isFullQuote)); url.searchParams.set("objectID", objectID.toString()); let response; try { 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 cd8e56d4ef3..0b3b71afcd7 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -63,8 +63,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve (0, Storage_1.markQuoteAsUsed)(this.#editorId, uuid); (0, Event_1.dispatchToCkeditor)(this.#editor).insertQuote({ author: message.author, - content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, - isText: quote.rawMessage === undefined, + content: quote.rawMessage ? quote.rawMessage : quote.message, + isText: !quote.rawMessage, link: message.link, }); }); 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 346e8053a3d..dc05b0e288b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -104,7 +104,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); } else { - await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); } removeSelection(); })); @@ -121,13 +121,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui quoteMessage = await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); } else { - quoteMessage = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + quoteMessage = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); } if (activeEditor !== undefined) { (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, + content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, + isText: !quoteMessage.rawMessage, link: quoteMessage.link, }); (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index e1bae83303c..1f5017fd0e5 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -7,7 +7,7 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Api/Messages/Author", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Ajax"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_1, Message_1, Ajax_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Ajax"], function (require, exports, tslib_1, Core, RenderQuote_1, List_1, ResetRemovalQuotes_1, Message_1, Ajax_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.legacySaveQuote = legacySaveQuote; @@ -45,8 +45,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C uuid, }; } - async function saveQuote(objectType, objectId, objectClassName, message) { - const result = await (0, Author_1.getMessageAuthor)(objectClassName, objectId); + async function saveQuote(objectType, objectId, message) { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, false); if (!result.ok) { throw new Error("Error fetching author data"); } @@ -85,7 +85,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C }; } async function saveFullQuote(objectType, objectId) { - const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId); + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, true); if (!result.ok) { throw new Error("Error fetching quote data"); } diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index e3205fd1895..5d5865c6ca8 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -152,7 +152,6 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\listViews\GetItem()); $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\messages\GetMessageAuthor()); $event->register(new \wcf\system\endpoint\controller\core\messages\ResetRemovalQuotes()); $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession()); $event->register(new \wcf\system\endpoint\controller\core\versionTrackers\RevertVersion()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php deleted file mode 100644 index d16228110f7..00000000000 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @since 6.2 - */ -#[GetRequest('/core/messages/message-author')] -final class GetMessageAuthor implements IController -{ - #[\Override] - public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface - { - $parameters = Helper::mapApiParameters($request, GetMessageAuthorParameters::class); - - // @phpstan-ignore argument.templateType - $object = Helper::fetchObjectFromRequestParameter($parameters->objectID, $parameters->className); - \assert($object instanceof IMessage && $object instanceof DatabaseObject); - - $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); - - return new JsonResponse( - [ - "objectID" => $object->getObjectID(), - "author" => $userProfile->getUsername(), - "avatar" => $userProfile->getAvatar()->getURL(), - "link" => $object->getLink(), - ], - 200, - [ - 'cache-control' => [ - 'max-age=300', - ], - ] - ); - } -} - -/** @internal */ -final class GetMessageAuthorParameters -{ - public function __construct( - /** @var non-empty-string */ - public readonly string $className, - /** @var positive-int */ - public readonly int $objectID, - ) {} -} From cc19e7aa314922275671c148a1aff6b0b9388bfd Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 17:17:31 +0200 Subject: [PATCH 06/10] Minor fixes --- ts/WoltLabSuite/Core/Component/Quote/Message.ts | 4 ++-- .../files/js/WoltLabSuite/Core/Component/Quote/Message.js | 4 ++-- .../endpoint/controller/core/messages/RenderQuote.class.php | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 0d8c015edd8..3d3a7ade49a 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -118,8 +118,8 @@ export function registerContainer( if (activeEditor !== undefined) { dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, + content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, + isText: !quoteMessage.rawMessage, link: quoteMessage.link, }); 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 dc05b0e288b..deeb67b4b99 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -69,8 +69,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui if (activeEditor !== undefined) { (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, + content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, + isText: !quoteMessage.rawMessage, link: quoteMessage.link, }); (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); 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 index 0e24f2c432b..e7b6d14c2fa 100644 --- 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 @@ -52,7 +52,6 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res return new JsonResponse([ 'objectID' => $parameters->objectID, - 'authorID' => $userProfile->getUserID(), 'author' => $userProfile->getUsername(), 'avatar' => $userProfile->getAvatar()->getURL(), 'link' => $message->getLink(), From 11fd64587e5845a7897ee76ea6cac62dfdcbfdef Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 17:55:33 +0200 Subject: [PATCH 07/10] Reintroduce the quote handler to simplify message fetching --- com.woltlab.wcf/objectTypeDefinition.xml | 1 + .../lib/data/IEmbeddedMessageObject.class.php | 19 --------- .../message/MessageQuoteRendering.class.php | 37 ----------------- .../core/messages/RenderQuote.class.php | 29 ++++++++----- .../AbstractMessageQuoteHandler.class.php | 41 +++++++++++++++---- .../quote/IMessageQuoteHandler.class.php | 28 +++++-------- 6 files changed, 61 insertions(+), 94 deletions(-) delete mode 100644 wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php delete mode 100644 wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 38524d0327e..8ea654c528d 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -27,6 +27,7 @@ com.woltlab.wcf.message.quote + wcf\system\message\quote\IMessageQuoteHandler com.woltlab.wcf.user.recentActivityEvent diff --git a/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php b/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php deleted file mode 100644 index da483e1eb68..00000000000 --- a/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -interface IEmbeddedMessageObject -{ - /** - * Loads embedded objects for the given object type and object IDs. - */ - public function loadEmbeddedObjects(): void; -} diff --git a/wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php b/wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php deleted file mode 100644 index c37c7a3b9e9..00000000000 --- a/wcfsetup/install/files/lib/event/message/MessageQuoteRendering.class.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @since 6.2 - */ -final class MessageQuoteRendering implements IPsr14Event -{ - private IMessage $message; - - public function __construct( - public readonly string $objectType, - public readonly int $objectID, - ) {} - - public function setMessage(IMessage $message): void - { - $this->message = $message; - } - - public function getMessage(): ?IMessage - { - return $this->message ?? null; - } -} 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 index e7b6d14c2fa..5991b154ac8 100644 --- 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 @@ -5,23 +5,23 @@ use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use wcf\data\IEmbeddedMessageObject; use wcf\data\IMessage; +use wcf\data\object\type\ObjectTypeCache; use wcf\data\user\UserProfile; -use wcf\event\message\MessageQuoteRendering; use wcf\http\Helper; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\endpoint\GetRequest; use wcf\system\endpoint\IController; -use wcf\system\event\EventHandler; +use wcf\system\exception\NotImplementedException; use wcf\system\exception\PermissionDeniedException; use wcf\system\html\input\HtmlInputProcessor; +use wcf\system\message\quote\IMessageQuoteHandler; /** * Retrieves data for the rendering of a quote. * * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.2 */ @@ -33,10 +33,21 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetRenderQuoteParameters::class); - $event = new MessageQuoteRendering($parameters->objectType, $parameters->objectID); - EventHandler::getInstance()->fire($event); + $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName( + 'com.woltlab.wcf.message.quote', + $parameters->objectType, + ); + $processor = $objectType->getProcessor(); + \assert($processor instanceof IMessageQuoteHandler); + + $message = null; + try { + $message = $processor->getMessage($parameters->objectID); + } catch (NotImplementedException) { + // This can happen for legacy implementations that do not yet + // implement the new `getMessage()` method. + } - $message = $event->getMessage(); if ($message === null) { throw new PermissionDeniedException(); } @@ -46,10 +57,6 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res $userProfile = UserProfile::getGuestUserProfile($message->getUsername()); } - if ($message instanceof IEmbeddedMessageObject) { - $message->loadEmbeddedObjects(); - } - return new JsonResponse([ 'objectID' => $parameters->objectID, 'author' => $userProfile->getUsername(), diff --git a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php index 56301d60597..d7a1548db27 100644 --- a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php @@ -2,35 +2,42 @@ namespace wcf\system\message\quote; +use wcf\data\IMessage; use wcf\system\cache\runtime\UserProfileRuntimeCache; +use wcf\system\exception\NotImplementedException; use wcf\system\SingletonFactory; use wcf\system\WCF; /** * Default implementation for quote handlers. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * - * @deprecated 6.2 + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License */ abstract class AbstractMessageQuoteHandler extends SingletonFactory implements IMessageQuoteHandler { /** * template name * @var string + * @deprecated 6.2 */ public $templateName = 'messageQuoteList'; /** * list of quoted message * @var QuotedMessage[] + * @deprecated 6.2 */ public $quotedMessages = []; /** - * @inheritDoc + * Renders a template for given quotes. + * + * @param mixed[][] $data + * @param bool $supportPaste + * @return string + * @deprecated 6.2 Implement `getMessage()` instead. */ public function render(array $data, $supportPaste = false) { @@ -58,8 +65,12 @@ public function render(array $data, $supportPaste = false) } /** - * @inheritDoc - * @param bool $renderAsString + * Renders a list of quotes for insertation. + * + * @param mixed[][] $data + * @param bool $render + * @return string[] + * @deprecated 6.2 Implement `getMessage()` instead. */ public function renderQuotes(array $data, $render = true, $renderAsString = true) { @@ -94,6 +105,7 @@ public function renderQuotes(array $data, $render = true, $renderAsString = true * * @param QuotedMessage[] $messages * @return void + * @deprecated 6.2 */ protected function overrideIsFullQuote(array $messages) { @@ -110,11 +122,16 @@ protected function overrideIsFullQuote(array $messages) * * @param mixed[][] $data * @return QuotedMessage[] + * @deprecated 6.2 Implement `getMessage()` instead. */ - abstract protected function getMessages(array $data); + protected function getMessages(array $data) + { + throw new NotImplementedException(); + } /** * @return list + * @deprecated 6.2 */ public function legacyGetMessages(int $objectID, string $marker): array { @@ -122,4 +139,10 @@ public function legacyGetMessages(int $objectID, string $marker): array $objectID => [$marker], ]); } + + #[\Override] + public function getMessage(int $objectID): ?IMessage + { + throw new NotImplementedException(); + } } diff --git a/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php index 8b6543815f7..8daee52f60d 100644 --- a/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php @@ -2,32 +2,24 @@ namespace wcf\system\message\quote; +use wcf\data\IMessage; + /** * Default interface for quote handlers. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * - * @deprecated 6.2 + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License */ interface IMessageQuoteHandler { /** - * Renders a template for given quotes. + * Returns the message identified by the provided object id. * - * @param mixed[][] $data - * @param bool $supportPaste - * @return string - */ - public function render(array $data, $supportPaste = false); - - /** - * Renders a list of quotes for insertation. + * If the object does not exist or is inaccessible by the current user, + * `null` must be returned instead. * - * @param mixed[][] $data - * @param bool $render - * @return string[] + * @since 6.2 */ - public function renderQuotes(array $data, $render = true); + public function getMessage(int $objectID): ?IMessage; } From 59a45e90f8971a5a8b1c2b6d2a4c4d66ae6066d9 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 18:36:28 +0200 Subject: [PATCH 08/10] Unify the implementations for full quotes --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 35 ++++++---- .../Core/Component/Quote/Message.ts | 29 ++++---- .../Core/Component/Quote/Storage.ts | 67 +++++-------------- ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 18 ++--- .../WoltLabSuite/Core/Component/Quote/List.js | 12 +++- .../Core/Component/Quote/Message.js | 23 +++---- .../Core/Component/Quote/Storage.js | 56 +++++----------- .../js/WoltLabSuite/Core/Ui/Message/Quote.js | 9 ++- 8 files changed, 96 insertions(+), 153 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 90f814a1efa..0c951898b33 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -78,9 +78,14 @@ class QuoteList { fragment.querySelector('button[data-action="insert"]')!.addEventListener("click", () => { markQuoteAsUsed(this.#editorId, uuid); + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } + dispatchToCkeditor(this.#editor).insertQuote({ author: message.author, - content: quote.rawMessage ? quote.rawMessage : quote.message, + content, isText: !quote.rawMessage, link: message.link, }); @@ -142,21 +147,23 @@ export function setup(editorId: string, containerId?: string): void { throw new Error(`The editor '${editorId}' does not exist.`); } - listenToCkeditor(editor).ready(({ ckeditor }) => { - if (ckeditor.features.quoteBlock) { - quoteLists.set(editorId, new QuoteList(editorId, editor, containerId)); - } - - if (ckeditor.isVisible()) { - setActiveEditor(ckeditor, ckeditor.features.quoteBlock); - } + listenToCkeditor(editor) + .ready(({ ckeditor }) => { + if (ckeditor.features.quoteBlock) { + quoteLists.set(editorId, new QuoteList(editorId, editor, containerId)); + } - ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => { - if (isFocused) { + if (ckeditor.isVisible()) { setActiveEditor(ckeditor, ckeditor.features.quoteBlock); } + + ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => { + if (isFocused) { + setActiveEditor(ckeditor, ckeditor.features.quoteBlock); + } + }); + }) + .destroy(() => { + removeActiveEditor(editor); }); - }).destroy(() => { - removeActiveEditor(editor); - }); } diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 3d3a7ade49a..7968d39302c 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -21,7 +21,6 @@ import { getKey, removeQuotes, legacySaveQuote, - legacySaveFullQuote, } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; @@ -30,8 +29,9 @@ type Container = { element: HTMLElement; messageBodySelector: string; objectType: string; - className: string; objectId: number; + /** @deprecated 6.2 Used for legacy implementations only. */ + className: string | undefined; }; let selectedMessage: @@ -59,8 +59,8 @@ const copyQuote = document.createElement("div"); export function registerContainer( containerSelector: string, messageBodySelector: string, - className: string, objectType: string, + className?: string, ): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); @@ -70,8 +70,8 @@ export function registerContainer( element: container, messageBodySelector, objectType, - className, objectId, + className, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -103,22 +103,22 @@ export function registerContainer( if (isFullQuoted(objectType, objectId)) { removeQuotes([getFullQuoteUuid(objectType, objectId)!]); quoteMessageButton!.classList.remove("active"); + return; } - // TODO - let quoteMessage: any; - if (className.endsWith("Action")) { - quoteMessage = await legacySaveFullQuote(objectType, className, ~~container.dataset.objectId!); - } else { - quoteMessage = await saveFullQuote(objectType, ~~container.dataset.objectId!); - } + const quoteMessage = await saveFullQuote(objectType, objectId, className); quoteMessageButton!.classList.add("active"); if (activeEditor !== undefined) { + const content = quoteMessage.rawMessage || quoteMessage.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } + dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, - content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, + content, isText: !quoteMessage.rawMessage, link: quoteMessage.link, }); @@ -160,7 +160,7 @@ function setup() { buttonSaveQuote.addEventListener( "click", promiseMutex(async () => { - if (selectedMessage!.container.className.endsWith("Action")) { + if (selectedMessage!.container.className) { await legacySaveQuote( selectedMessage!.container.objectType, selectedMessage!.container.objectId, @@ -179,6 +179,7 @@ function setup() { }), ); copyQuote.appendChild(buttonSaveQuote); + const buttonSaveAndInsertQuote = document.createElement("button"); buttonSaveAndInsertQuote.type = "button"; buttonSaveAndInsertQuote.hidden = true; @@ -189,7 +190,7 @@ function setup() { promiseMutex(async () => { // TODO let quoteMessage: any; - if (selectedMessage!.container.className.endsWith("Action")) { + if (selectedMessage!.container.className) { quoteMessage = await legacySaveQuote( selectedMessage!.container.objectType, selectedMessage!.container.objectId, diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 3b999b7269f..38d8e99b4ba 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -23,7 +23,7 @@ interface Message { } interface Quote { - message: string; + message: string | null; rawMessage?: string | null; } @@ -35,7 +35,7 @@ interface StorageData { type LegacyQuoteData = { count: number; fullQuoteMessageIDs: number[]; - renderedQuote: Message; + renderedQuote: Message & Quote; }; const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; @@ -91,65 +91,32 @@ export async function saveQuote( }; } -export async function legacySaveFullQuote( +export async function saveFullQuote( objectType: string, - objectClassName: string, objectId: number, + /** @deprecated 6.2 Used for legacy implementations only. */ + className?: string, ): Promise { - const result = (await dboAction("saveFullQuote", objectClassName) - .objectIds([objectId]) - .dispatch()) as LegacyQuoteData; - - const message = { - objectID: result.renderedQuote.objectID, - link: result.renderedQuote.link, - author: result.renderedQuote.author, - avatar: result.renderedQuote.avatar, - }; - - const quote = { - // TODO - message: (result.renderedQuote as any).message, - // TODO - rawMessage: (result.renderedQuote as any).rawMessage, - }; - - const uuid = storeQuote(objectType, message, quote); - - refreshQuoteLists(); - - return { - ...message, - ...quote, - uuid, - }; -} + let message: Message & Quote; + + if (className !== undefined) { + const result = (await dboAction("saveFullQuote", className).objectIds([objectId]).dispatch()) as LegacyQuoteData; + message = result.renderedQuote; + } else { + const result = await renderQuote(objectType, objectId, true); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } -export async function saveFullQuote(objectType: string, objectId: number): Promise { - const result = await renderQuote(objectType, objectId, true); - if (!result.ok) { - throw new Error("Error fetching quote data"); + message = result.value; } - const message = { - objectID: result.value.objectID, - link: result.value.link, - author: result.value.author, - avatar: result.value.avatar, - }; - - const quote = { - message: result.value.message!, - rawMessage: result.value.rawMessage!, - }; - - const uuid = storeQuote(objectType, message, quote); + const uuid = storeQuote(objectType, message, message); refreshQuoteLists(); return { ...message, - ...quote, uuid, }; } diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index 2137e3e821b..c039368b0d1 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -6,18 +6,15 @@ import { registerContainer } from "WoltLabSuite/Core/Component/Quote/Message"; -// see WCF.Message.Quote.Manager -export interface WCFMessageQuoteManager { - supportPaste: () => boolean; - updateCount: (number, object) => void; -} - +/** + * @deprecated 6.2 Use `registerContainer()` without the className parameter. + */ export class UiMessageQuote { /** * Initializes the quote handler for given object type. */ constructor( - _quoteManager: WCFMessageQuoteManager, + _quoteManager: typeof window.WCF.Message.Quote.Manager, className: string, objectType: string, containerSelector: string, @@ -25,12 +22,7 @@ export class UiMessageQuote { _messageContentSelector: string, _supportDirectInsert: boolean, ) { - // remove "Action" from className - /*if (className.endsWith("Action")) { - className = className.substring(0, className.length - 6); - }*/ - - registerContainer(containerSelector, messageBodySelector, className, objectType); + registerContainer(containerSelector, messageBodySelector, objectType, className); } } 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 0b3b71afcd7..1be071b13bb 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -61,9 +61,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve `); fragment.querySelector('button[data-action="insert"]').addEventListener("click", () => { (0, Storage_1.markQuoteAsUsed)(this.#editorId, uuid); + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } (0, Event_1.dispatchToCkeditor)(this.#editor).insertQuote({ author: message.author, - content: quote.rawMessage ? quote.rawMessage : quote.message, + content, isText: !quote.rawMessage, link: message.link, }); @@ -110,7 +114,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve if (editor === null) { throw new Error(`The editor '${editorId}' does not exist.`); } - (0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => { + (0, Event_1.listenToCkeditor)(editor) + .ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, editor, containerId)); } @@ -122,7 +127,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); } }); - }).destroy(() => { + }) + .destroy(() => { (0, Message_1.removeActiveEditor)(editor); }); } 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 deeb67b4b99..038efa818e0 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -23,7 +23,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui let timerSelectionChange = undefined; let isMouseDown = false; const copyQuote = document.createElement("div"); - function registerContainer(containerSelector, messageBodySelector, className, objectType) { + function registerContainer(containerSelector, messageBodySelector, objectType, className) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); const objectId = ~~container.dataset.objectId; @@ -31,8 +31,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui element: container, messageBodySelector, objectType, - className, objectId, + className, }); if (container.classList.contains("jsInvalidQuoteTarget")) { return; @@ -57,19 +57,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui quoteMessageButton.classList.remove("active"); return; } - // TODO - let quoteMessage; - if (className.endsWith("Action")) { - quoteMessage = await (0, Storage_1.legacySaveFullQuote)(objectType, className, ~~container.dataset.objectId); - } - else { - quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, ~~container.dataset.objectId); - } + const quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, objectId, className); quoteMessageButton.classList.add("active"); if (activeEditor !== undefined) { + const content = quoteMessage.rawMessage || quoteMessage.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ author: quoteMessage.author, - content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, + content, isText: !quoteMessage.rawMessage, link: quoteMessage.link, }); @@ -100,7 +97,7 @@ 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", (0, PromiseMutex_1.promiseMutex)(async () => { - if (selectedMessage.container.className.endsWith("Action")) { + if (selectedMessage.container.className) { await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); } else { @@ -117,7 +114,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveAndInsertQuote.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { // TODO let quoteMessage; - if (selectedMessage.container.className.endsWith("Action")) { + if (selectedMessage.container.className) { quoteMessage = await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); } else { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index 1f5017fd0e5..0f37c6f6fd6 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -12,7 +12,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C Object.defineProperty(exports, "__esModule", { value: true }); exports.legacySaveQuote = legacySaveQuote; exports.saveQuote = saveQuote; - exports.legacySaveFullQuote = legacySaveFullQuote; exports.saveFullQuote = saveFullQuote; exports.getQuotes = getQuotes; exports.getMessage = getMessage; @@ -60,50 +59,25 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C uuid, }; } - async function legacySaveFullQuote(objectType, objectClassName, objectId) { - const result = (await (0, Ajax_1.dboAction)("saveFullQuote", objectClassName) - .objectIds([objectId]) - .dispatch()); - const message = { - objectID: result.renderedQuote.objectID, - link: result.renderedQuote.link, - author: result.renderedQuote.author, - avatar: result.renderedQuote.avatar, - }; - const quote = { - // TODO - message: result.renderedQuote.message, - // TODO - rawMessage: result.renderedQuote.rawMessage, - }; - const uuid = storeQuote(objectType, message, quote); - (0, List_1.refreshQuoteLists)(); - return { - ...message, - ...quote, - uuid, - }; - } - async function saveFullQuote(objectType, objectId) { - const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, true); - if (!result.ok) { - throw new Error("Error fetching quote data"); + async function saveFullQuote(objectType, objectId, + /** @deprecated 6.2 Used for legacy implementations only. */ + className) { + let message; + if (className !== undefined) { + const result = (await (0, Ajax_1.dboAction)("saveFullQuote", className).objectIds([objectId]).dispatch()); + message = result.renderedQuote; } - const message = { - objectID: result.value.objectID, - link: result.value.link, - author: result.value.author, - avatar: result.value.avatar, - }; - const quote = { - message: result.value.message, - rawMessage: result.value.rawMessage, - }; - const uuid = storeQuote(objectType, message, quote); + else { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, true); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } + message = result.value; + } + const uuid = storeQuote(objectType, message, message); (0, List_1.refreshQuoteLists)(); return { ...message, - ...quote, uuid, }; } 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 9cfb8620455..6e25bf60374 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -7,16 +7,15 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UiMessageQuote = void 0; + /** + * @deprecated 6.2 Use `registerContainer()` without the className parameter. + */ class UiMessageQuote { /** * Initializes the quote handler for given object type. */ constructor(_quoteManager, className, objectType, containerSelector, messageBodySelector, _messageContentSelector, _supportDirectInsert) { - // remove "Action" from className - /*if (className.endsWith("Action")) { - className = className.substring(0, className.length - 6); - }*/ - (0, Message_1.registerContainer)(containerSelector, messageBodySelector, className, objectType); + (0, Message_1.registerContainer)(containerSelector, messageBodySelector, objectType, className); } } exports.UiMessageQuote = UiMessageQuote; From 40497f917fa2007715725722020e7f57ff02904f Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 18:47:11 +0200 Subject: [PATCH 09/10] Simplify the code a bit --- .../Core/Component/Quote/Message.ts | 38 ++++++++++--------- .../Core/Component/Quote/Storage.ts | 27 +++---------- .../Core/Component/Quote/Message.js | 36 ++++++++++-------- .../Core/Component/Quote/Storage.js | 23 +++-------- 4 files changed, 53 insertions(+), 71 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 7968d39302c..933d39741d8 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -17,7 +17,6 @@ import { getFullQuoteUuid, saveFullQuote, markQuoteAsUsed, - isFullQuoted, getKey, removeQuotes, legacySaveQuote, @@ -64,7 +63,7 @@ export function registerContainer( ): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); - const objectId = ~~container.dataset.objectId!; + const objectId = parseInt(container.dataset.objectId || "0"); containers.set(id, { element: container, @@ -82,48 +81,53 @@ export function registerContainer( container.classList.add("jsQuoteMessageContainer"); const quoteMessage = container.querySelector(".jsQuoteMessage"); - let quoteMessageButton = quoteMessage?.querySelector(".button"); - if (!quoteMessageButton && quoteMessage?.classList.contains("button")) { + if (quoteMessage === null) { + return; + } + + let quoteMessageButton = quoteMessage.querySelector(".button"); + if (!quoteMessageButton && quoteMessage.classList.contains("button")) { quoteMessageButton = quoteMessage; } - if (quoteMessageButton) { + if (quoteMessageButton !== null) { quoteMessageButtons.set(getKey(objectType, objectId), quoteMessageButton); - if (isFullQuoted(objectType, objectId)) { + if (getFullQuoteUuid(objectType, objectId) !== undefined) { quoteMessageButton.classList.add("active"); } } - quoteMessage?.addEventListener( + quoteMessage.addEventListener( "click", promiseMutex(async (event: MouseEvent) => { event.preventDefault(); - if (isFullQuoted(objectType, objectId)) { - removeQuotes([getFullQuoteUuid(objectType, objectId)!]); - quoteMessageButton!.classList.remove("active"); + const uuid = getFullQuoteUuid(objectType, objectId); + if (uuid !== undefined) { + removeQuotes([uuid]); + quoteMessageButton?.classList.remove("active"); return; } - const quoteMessage = await saveFullQuote(objectType, objectId, className); - quoteMessageButton!.classList.add("active"); + const quote = await saveFullQuote(objectType, objectId, className); + quoteMessageButton?.classList.add("active"); if (activeEditor !== undefined) { - const content = quoteMessage.rawMessage || quoteMessage.message; + const content = quote.rawMessage || quote.message; if (content === null) { throw new Error("Expected either the `rawMessage` or `message` to be a string."); } dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, + author: quote.author, content, - isText: !quoteMessage.rawMessage, - link: quoteMessage.link, + isText: !quote.rawMessage, + link: quote.link, }); - markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); + markQuoteAsUsed(activeEditor.sourceElement.id, quote.uuid); } }), ); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 38d8e99b4ba..68bfa2624e3 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -215,24 +215,6 @@ export function clearQuotesForEditor(editorId: string): void { }); } -export function isFullQuoted(objectType: string, objectId: number): boolean { - const key = getKey(objectType, objectId); - const storage = getStorage(); - const quotes = storage.quotes.get(key); - - if (quotes === undefined) { - return false; - } - - return ( - Array.from(quotes).filter(([, quote]) => { - if (quote.rawMessage !== undefined) { - return true; - } - }).length > 0 - ); -} - function storeQuote(objectType: string, message: Message, quote: Quote): string { const storage = getStorage(); @@ -258,11 +240,14 @@ function storeQuote(objectType: string, message: Message, quote: Quote): string } export function getFullQuoteUuid(objectType: string, objectId: number): string | undefined { - const storage = getStorage(); const key = getKey(objectType, objectId); + const quotes = getStorage().quotes.get(key); + if (quotes === undefined) { + return undefined; + } - for (const [uuid, q] of storage.quotes.get(key)!) { - if (q.rawMessage !== undefined && q.message !== undefined) { + for (const [uuid, q] of quotes) { + if (q.rawMessage && q.message) { return uuid; } } 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 038efa818e0..6daab259238 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -26,7 +26,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui function registerContainer(containerSelector, messageBodySelector, objectType, className) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); - const objectId = ~~container.dataset.objectId; + const objectId = parseInt(container.dataset.objectId || "0"); containers.set(id, { element: container, messageBodySelector, @@ -40,37 +40,41 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); const quoteMessage = container.querySelector(".jsQuoteMessage"); - let quoteMessageButton = quoteMessage?.querySelector(".button"); - if (!quoteMessageButton && quoteMessage?.classList.contains("button")) { + if (quoteMessage === null) { + return; + } + let quoteMessageButton = quoteMessage.querySelector(".button"); + if (!quoteMessageButton && quoteMessage.classList.contains("button")) { quoteMessageButton = quoteMessage; } - if (quoteMessageButton) { + if (quoteMessageButton !== null) { quoteMessageButtons.set((0, Storage_1.getKey)(objectType, objectId), quoteMessageButton); - if ((0, Storage_1.isFullQuoted)(objectType, objectId)) { + if ((0, Storage_1.getFullQuoteUuid)(objectType, objectId) !== undefined) { quoteMessageButton.classList.add("active"); } } - quoteMessage?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { + quoteMessage.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { event.preventDefault(); - if ((0, Storage_1.isFullQuoted)(objectType, objectId)) { - (0, Storage_1.removeQuotes)([(0, Storage_1.getFullQuoteUuid)(objectType, objectId)]); - quoteMessageButton.classList.remove("active"); + const uuid = (0, Storage_1.getFullQuoteUuid)(objectType, objectId); + if (uuid !== undefined) { + (0, Storage_1.removeQuotes)([uuid]); + quoteMessageButton?.classList.remove("active"); return; } - const quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, objectId, className); - quoteMessageButton.classList.add("active"); + const quote = await (0, Storage_1.saveFullQuote)(objectType, objectId, className); + quoteMessageButton?.classList.add("active"); if (activeEditor !== undefined) { - const content = quoteMessage.rawMessage || quoteMessage.message; + const content = quote.rawMessage || quote.message; if (content === null) { throw new Error("Expected either the `rawMessage` or `message` to be a string."); } (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, + author: quote.author, content, - isText: !quoteMessage.rawMessage, - link: quoteMessage.link, + isText: !quote.rawMessage, + link: quote.link, }); - (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); + (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quote.uuid); } })); }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index 0f37c6f6fd6..b1cd244f413 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -20,7 +20,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C exports.markQuoteAsUsed = markQuoteAsUsed; exports.getUsedQuotes = getUsedQuotes; exports.clearQuotesForEditor = clearQuotesForEditor; - exports.isFullQuoted = isFullQuoted; exports.getFullQuoteUuid = getFullQuoteUuid; exports.getKey = getKey; Core = tslib_1.__importStar(Core); @@ -152,19 +151,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C (0, Message_1.removeQuoteStatus)(key); }); } - function isFullQuoted(objectType, objectId) { - const key = getKey(objectType, objectId); - const storage = getStorage(); - const quotes = storage.quotes.get(key); - if (quotes === undefined) { - return false; - } - return (Array.from(quotes).filter(([, quote]) => { - if (quote.rawMessage !== undefined) { - return true; - } - }).length > 0); - } function storeQuote(objectType, message, quote) { const storage = getStorage(); const key = getKey(objectType, message.objectID); @@ -183,10 +169,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C return uuid; } function getFullQuoteUuid(objectType, objectId) { - const storage = getStorage(); const key = getKey(objectType, objectId); - for (const [uuid, q] of storage.quotes.get(key)) { - if (q.rawMessage !== undefined && q.message !== undefined) { + const quotes = getStorage().quotes.get(key); + if (quotes === undefined) { + return undefined; + } + for (const [uuid, q] of quotes) { + if (q.rawMessage && q.message) { return uuid; } } From 04e1412585be0d86c74f293417368ac7a42ac83d Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 3 Aug 2025 20:03:31 +0200 Subject: [PATCH 10/10] Merge the partial quotes implementation, improve type safety --- .../Core/Component/Quote/Message.ts | 64 +++++++++---------- .../Core/Component/Quote/Storage.ts | 63 ++++++++---------- .../Core/Component/Quote/Message.js | 34 +++++----- .../Core/Component/Quote/Storage.js | 56 ++++++++-------- 4 files changed, 99 insertions(+), 118 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 933d39741d8..30606ee7dd1 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -19,7 +19,6 @@ import { markQuoteAsUsed, getKey, removeQuotes, - legacySaveQuote, } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; @@ -123,7 +122,7 @@ export function registerContainer( dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ author: quote.author, content, - isText: !quote.rawMessage, + isText: quote.rawMessage === null, link: quote.link, }); @@ -164,21 +163,17 @@ function setup() { buttonSaveQuote.addEventListener( "click", promiseMutex(async () => { - if (selectedMessage!.container.className) { - await legacySaveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.container.className, - selectedMessage!.message, - ); - } else { - await saveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.message, - ); + if (selectedMessage === undefined) { + return; } + await saveQuote( + selectedMessage.container.objectType, + selectedMessage.container.objectId, + selectedMessage.message, + selectedMessage.container.className, + ); + removeSelection(); }), ); @@ -192,32 +187,31 @@ function setup() { buttonSaveAndInsertQuote.addEventListener( "click", promiseMutex(async () => { - // TODO - let quoteMessage: any; - if (selectedMessage!.container.className) { - quoteMessage = await legacySaveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.container.className, - selectedMessage!.message, - ); - } else { - quoteMessage = await saveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.message, - ); + if (selectedMessage === undefined) { + return; } + const quote = await saveQuote( + selectedMessage.container.objectType, + selectedMessage.container.objectId, + selectedMessage.message, + selectedMessage.container.className, + ); + if (activeEditor !== undefined) { + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } + dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, - content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, - isText: !quoteMessage.rawMessage, - link: quoteMessage.link, + author: quote.author, + content, + isText: quote.rawMessage === null, + link: quote.link, }); - markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); + markQuoteAsUsed(activeEditor.sourceElement.id, quote.uuid); } removeSelection(); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 68bfa2624e3..38bbb9f18b9 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -24,7 +24,7 @@ interface Message { interface Quote { message: string | null; - rawMessage?: string | null; + rawMessage: string | null; } interface StorageData { @@ -41,51 +41,42 @@ type LegacyQuoteData = { const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map>(); -export async function legacySaveQuote( +export async function saveQuote( objectType: string, objectId: number, - className: string, message: string, + /** @deprecated 6.2 Used for legacy implementations only. */ + className?: string, ): Promise { - const result = (await dboAction("saveQuote", className) - .objectIds([objectId]) - .payload({ - message, - renderQuote: true, - }) - .dispatch()) as LegacyQuoteData; - - const uuid = storeQuote(objectType, result.renderedQuote, { - message, - }); + let quote: Message & Quote; - refreshQuoteLists(); - - return { - ...result.renderedQuote, - message, - uuid, - }; -} + if (className !== undefined) { + const result = (await dboAction("saveQuote", className) + .objectIds([objectId]) + .payload({ + message, + renderQuote: true, + }) + .dispatch()) as LegacyQuoteData; + quote = result.renderedQuote; + } else { + const result = await renderQuote(objectType, objectId, false); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } -export async function saveQuote( - objectType: string, - objectId: number, - message: string, -): Promise { - const result = await renderQuote(objectType, objectId, false); - if (!result.ok) { - throw new Error("Error fetching author data"); + quote = result.value; } - const uuid = storeQuote(objectType, result.value, { + const uuid = storeQuote(objectType, quote, { message, + rawMessage: null, }); refreshQuoteLists(); return { - ...result.value, + ...quote, message, uuid, }; @@ -190,7 +181,7 @@ export function clearQuotesForEditor(editorId: string): void { usedQuotes.get(editorId)?.forEach((uuid) => { for (const [key, quotes] of storage.quotes) { const quote = quotes.get(uuid); - if (quote?.rawMessage !== undefined) { + if (quote?.rawMessage !== null) { fullQuotes.push(key); } @@ -226,7 +217,7 @@ function storeQuote(objectType: string, message: Message, quote: Quote): string storage.messages.set(key, message); for (const [uuid, q] of storage.quotes.get(key)!) { - if ((q.rawMessage !== undefined && q.rawMessage === quote.rawMessage) || q.message === quote.message) { + if ((q.rawMessage !== null && q.rawMessage === null) || q.message === quote.message) { return uuid; } } @@ -247,7 +238,7 @@ export function getFullQuoteUuid(objectType: string, objectId: number): string | } for (const [uuid, q] of quotes) { - if (q.rawMessage && q.message) { + if (q.rawMessage !== null && q.message !== null) { return uuid; } } @@ -311,7 +302,7 @@ window.addEventListener("storage", (event) => { // Update the quote status if the quote was removed in another tab for (const [key, quotes] of oldValue.quotes) { for (const [, quote] of quotes) { - if (quote.rawMessage !== undefined && !newValue.quotes.has(key)) { + if (quote.rawMessage !== null && !newValue.quotes.has(key)) { removeQuoteStatus(key); } } 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 6daab259238..044b833170a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -71,7 +71,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ author: quote.author, content, - isText: !quote.rawMessage, + isText: quote.rawMessage === null, link: quote.link, }); (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quote.uuid); @@ -101,12 +101,10 @@ 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", (0, PromiseMutex_1.promiseMutex)(async () => { - if (selectedMessage.container.className) { - await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); - } - else { - await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + if (selectedMessage === undefined) { + return; } + await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message, selectedMessage.container.className); removeSelection(); })); copyQuote.appendChild(buttonSaveQuote); @@ -116,22 +114,22 @@ 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", (0, PromiseMutex_1.promiseMutex)(async () => { - // TODO - let quoteMessage; - if (selectedMessage.container.className) { - quoteMessage = await (0, Storage_1.legacySaveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); - } - else { - quoteMessage = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + if (selectedMessage === undefined) { + return; } + const quote = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message, selectedMessage.container.className); if (activeEditor !== undefined) { + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, - content: quoteMessage.rawMessage ? quoteMessage.rawMessage : quoteMessage.message, - isText: !quoteMessage.rawMessage, - link: quoteMessage.link, + author: quote.author, + content, + isText: quote.rawMessage === null, + link: quote.link, }); - (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); + (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quote.uuid); } removeSelection(); })); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index b1cd244f413..a40a03e253a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -10,7 +10,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Ajax"], function (require, exports, tslib_1, Core, RenderQuote_1, List_1, ResetRemovalQuotes_1, Message_1, Ajax_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.legacySaveQuote = legacySaveQuote; exports.saveQuote = saveQuote; exports.saveFullQuote = saveFullQuote; exports.getQuotes = getQuotes; @@ -25,35 +24,34 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C Core = tslib_1.__importStar(Core); const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map(); - async function legacySaveQuote(objectType, objectId, className, message) { - const result = (await (0, Ajax_1.dboAction)("saveQuote", className) - .objectIds([objectId]) - .payload({ - message, - renderQuote: true, - }) - .dispatch()); - const uuid = storeQuote(objectType, result.renderedQuote, { - message, - }); - (0, List_1.refreshQuoteLists)(); - return { - ...result.renderedQuote, - message, - uuid, - }; - } - async function saveQuote(objectType, objectId, message) { - const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, false); - if (!result.ok) { - throw new Error("Error fetching author data"); + async function saveQuote(objectType, objectId, message, + /** @deprecated 6.2 Used for legacy implementations only. */ + className) { + let quote; + if (className !== undefined) { + const result = (await (0, Ajax_1.dboAction)("saveQuote", className) + .objectIds([objectId]) + .payload({ + message, + renderQuote: true, + }) + .dispatch()); + quote = result.renderedQuote; + } + else { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, false); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } + quote = result.value; } - const uuid = storeQuote(objectType, result.value, { + const uuid = storeQuote(objectType, quote, { message, + rawMessage: null, }); (0, List_1.refreshQuoteLists)(); return { - ...result.value, + ...quote, message, uuid, }; @@ -132,7 +130,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C usedQuotes.get(editorId)?.forEach((uuid) => { for (const [key, quotes] of storage.quotes) { const quote = quotes.get(uuid); - if (quote?.rawMessage !== undefined) { + if (quote?.rawMessage !== null) { fullQuotes.push(key); } quotes.delete(uuid); @@ -159,7 +157,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C } storage.messages.set(key, message); for (const [uuid, q] of storage.quotes.get(key)) { - if ((q.rawMessage !== undefined && q.rawMessage === quote.rawMessage) || q.message === quote.message) { + if ((q.rawMessage !== null && q.rawMessage === null) || q.message === quote.message) { return uuid; } } @@ -175,7 +173,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C return undefined; } for (const [uuid, q] of quotes) { - if (q.rawMessage && q.message) { + if (q.rawMessage !== null && q.message !== null) { return uuid; } } @@ -224,7 +222,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C // Update the quote status if the quote was removed in another tab for (const [key, quotes] of oldValue.quotes) { for (const [, quote] of quotes) { - if (quote.rawMessage !== undefined && !newValue.quotes.has(key)) { + if (quote.rawMessage !== null && !newValue.quotes.has(key)) { (0, Message_1.removeQuoteStatus)(key); } }