From c10a4c5b7cee400c9c9549a7edf4251157225cb5 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 17 Dec 2024 10:33:28 +0100 Subject: [PATCH 01/65] Add basic tabs for quote --- com.woltlab.wcf/templates/__messageFormQuote.tpl | 5 +++++ com.woltlab.wcf/templates/__messageFormQuoteInline.tpl | 5 +++++ com.woltlab.wcf/templates/messageFormTabs.tpl | 10 ++++++++++ com.woltlab.wcf/templates/messageFormTabsInline.tpl | 10 ++++++++++ 4 files changed, 30 insertions(+) create mode 100644 com.woltlab.wcf/templates/__messageFormQuote.tpl create mode 100644 com.woltlab.wcf/templates/__messageFormQuoteInline.tpl diff --git a/com.woltlab.wcf/templates/__messageFormQuote.tpl b/com.woltlab.wcf/templates/__messageFormQuote.tpl new file mode 100644 index 0000000000..30e8947507 --- /dev/null +++ b/com.woltlab.wcf/templates/__messageFormQuote.tpl @@ -0,0 +1,5 @@ +{* TODO *} + +
+ +
diff --git a/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl b/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl new file mode 100644 index 0000000000..30e8947507 --- /dev/null +++ b/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl @@ -0,0 +1,5 @@ +{* TODO *} + +
+ +
diff --git a/com.woltlab.wcf/templates/messageFormTabs.tpl b/com.woltlab.wcf/templates/messageFormTabs.tpl index bfc4726dc0..3fbf13842c 100644 --- a/com.woltlab.wcf/templates/messageFormTabs.tpl +++ b/com.woltlab.wcf/templates/messageFormTabs.tpl @@ -41,6 +41,14 @@ {/if} {event name='tabMenuTabs'} + +
  • + {* TODO change count *} + +
  • @@ -53,4 +61,6 @@ {include file='__messageFormPoll'} {event name='tabMenuContents'} + + {include file='__messageFormQuote'} \ No newline at end of file diff --git a/com.woltlab.wcf/templates/messageFormTabsInline.tpl b/com.woltlab.wcf/templates/messageFormTabsInline.tpl index 71c23dbcb2..ec9e0de82e 100644 --- a/com.woltlab.wcf/templates/messageFormTabsInline.tpl +++ b/com.woltlab.wcf/templates/messageFormTabsInline.tpl @@ -44,6 +44,14 @@ {/if} {event name='tabMenuTabs'} + +
  • + {* TODO change count *} + +
  • @@ -57,4 +65,6 @@ {include file='__messageFormPollInline'} {event name='tabMenuContents'} + + {include file='__messageFormQuoteInline'} \ No newline at end of file From d2adc8d1101362652e04f8dafd4c1e94a4c75e2d Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 17 Dec 2024 13:48:11 +0100 Subject: [PATCH 02/65] Render quote tab dynamic --- .../templates/__messageFormQuote.tpl | 8 +- .../templates/__messageFormQuoteInline.tpl | 8 +- com.woltlab.wcf/templates/messageFormTabs.tpl | 5 +- .../templates/messageFormTabsInline.tpl | 5 +- .../Core/Component/Message/MessageTabMenu.ts | 25 ++++++ ts/WoltLabSuite/Core/Component/Quote/List.ts | 77 +++++++++++++++++++ .../Core/Component/Message/MessageTabMenu.js | 21 +++++ .../WoltLabSuite/Core/Component/Quote/List.js | 58 ++++++++++++++ ...PreloadPhrasesCollectingListener.class.php | 2 + 9 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Component/Quote/List.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js diff --git a/com.woltlab.wcf/templates/__messageFormQuote.tpl b/com.woltlab.wcf/templates/__messageFormQuote.tpl index 30e8947507..d4b635e106 100644 --- a/com.woltlab.wcf/templates/__messageFormQuote.tpl +++ b/com.woltlab.wcf/templates/__messageFormQuote.tpl @@ -1,5 +1,11 @@ {* TODO *} -
    +
    + + diff --git a/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl b/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl index 30e8947507..d4b635e106 100644 --- a/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl +++ b/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl @@ -1,5 +1,11 @@ {* TODO *} -
    +
    + + diff --git a/com.woltlab.wcf/templates/messageFormTabs.tpl b/com.woltlab.wcf/templates/messageFormTabs.tpl index 3fbf13842c..90b8083122 100644 --- a/com.woltlab.wcf/templates/messageFormTabs.tpl +++ b/com.woltlab.wcf/templates/messageFormTabs.tpl @@ -42,11 +42,10 @@ {/if} {event name='tabMenuTabs'} -
  • - {* TODO change count *} +
  • diff --git a/com.woltlab.wcf/templates/messageFormTabsInline.tpl b/com.woltlab.wcf/templates/messageFormTabsInline.tpl index ec9e0de82e..e05eefa86d 100644 --- a/com.woltlab.wcf/templates/messageFormTabsInline.tpl +++ b/com.woltlab.wcf/templates/messageFormTabsInline.tpl @@ -45,11 +45,10 @@ {/if} {event name='tabMenuTabs'} -
  • - {* TODO change count *} +
  • diff --git a/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts b/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts index 0d3d4d7579..3b38892d18 100644 --- a/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts +++ b/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts @@ -65,6 +65,31 @@ class TabMenu { this.#activeTabName = tabName; } + showTab(tabName: string, title?: string): void { + this.#tabs + .filter((element) => element.dataset.name === tabName) + .forEach((element) => { + element.hidden = false; + + // Set new title + if (title) { + element.querySelector("span")!.textContent = title; + } + }); + } + + hideTab(tabName: string): void { + this.#tabs + .filter((element) => element.dataset.name === tabName) + .forEach((element) => { + element.hidden = true; + + if (element.classList.contains("active")) { + this.#closeAllTabs(); + } + }); + } + setTabCounter(tabName: string, value: number): void { const tab = this.#tabs.find((element) => element.dataset.name === tabName); if (tab === undefined) { diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts new file mode 100644 index 0000000000..27604f26b6 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -0,0 +1,77 @@ +/** + * Handles quotes for CKEditor 5 message fields. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ +import * as Core from "WoltLabSuite/Core/Core"; +import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; +import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; +import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; +import { getPhrase } from "WoltLabSuite/Core/Language"; + +export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; +const quoteLists = new Map(); + +class QuoteList { + #container: HTMLElement; + #editor: CKEditor; + #editorId: string; + + constructor(editorId: string, editor: CKEditor) { + this.#editorId = editorId; + this.#editor = editor; + this.#container = document.getElementById(`quotes_${editorId}`)!; + if (this.#container === null) { + throw new Error(`The quotes container for '${editorId}' does not exist.`); + } + + window.addEventListener("storage", (event) => { + if (event.key !== STORAGE_KEY) { + return; + } + + this.renderQuotes(event.newValue); + }); + + this.renderQuotes(window.localStorage.getItem(STORAGE_KEY)); + } + + public renderQuotes(template: string | null): void { + this.#container.innerHTML = template || ""; + + if (template) { + getTabMenu(this.#editorId)?.showTab( + "quotes", + getPhrase("wcf.message.quote.showQuotes", { + count: this.#container.childElementCount, + }), + ); + } else { + getTabMenu(this.#editorId)?.hideTab("quotes"); + } + } +} + +export function getQuoteList(editorId: string): QuoteList | undefined { + return quoteLists.get(editorId); +} + +export function setup(editorId: string): void { + if (quoteLists.has(editorId)) { + return; + } + + const editor = document.getElementById(editorId); + if (editor === null) { + throw new Error(`The editor '${editorId}' does not exist.`); + } + + listenToCkeditor(editor).ready(({ ckeditor }) => { + if (ckeditor.features.quoteBlock) { + quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); + } + }); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js index fcbfb6d4fe..7974eff75d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js @@ -53,6 +53,27 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol this.#tabContainers[tabIndex].classList.add("active"); this.#activeTabName = tabName; } + showTab(tabName, title) { + this.#tabs + .filter((element) => element.dataset.name === tabName) + .forEach((element) => { + element.hidden = false; + // Set new title + if (title) { + element.querySelector("span").textContent = title; + } + }); + } + hideTab(tabName) { + this.#tabs + .filter((element) => element.dataset.name === tabName) + .forEach((element) => { + element.hidden = true; + if (element.classList.contains("active")) { + this.#closeAllTabs(); + } + }); + } setTabCounter(tabName, value) { const tab = this.#tabs.find((element) => element.dataset.name === tabName); if (tab === undefined) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js new file mode 100644 index 0000000000..c00ea38c31 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -0,0 +1,58 @@ +define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.STORAGE_KEY = void 0; + exports.getQuoteList = getQuoteList; + exports.setup = setup; + Core = tslib_1.__importStar(Core); + exports.STORAGE_KEY = Core.getStoragePrefix() + "quotes"; + const quoteLists = new Map(); + class QuoteList { + #container; + #editor; + #editorId; + constructor(editorId, editor) { + this.#editorId = editorId; + this.#editor = editor; + this.#container = document.getElementById(`quotes_${editorId}`); + if (this.#container === null) { + throw new Error(`The quotes container for '${editorId}' does not exist.`); + } + window.addEventListener("storage", (event) => { + if (event.key !== exports.STORAGE_KEY) { + return; + } + this.renderQuotes(event.newValue); + }); + this.renderQuotes(window.localStorage.getItem(exports.STORAGE_KEY)); + } + renderQuotes(template) { + this.#container.innerHTML = template || ""; + if (template) { + (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { + count: this.#container.childElementCount, + })); + } + else { + (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.hideTab("quotes"); + } + } + } + function getQuoteList(editorId) { + return quoteLists.get(editorId); + } + function setup(editorId) { + if (quoteLists.has(editorId)) { + return; + } + const editor = document.getElementById(editorId); + if (editor === null) { + throw new Error(`The editor '${editorId}' does not exist.`); + } + (0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => { + if (ckeditor.features.quoteBlock) { + quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); + } + }); + } +}); diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index 3c6626ee1f..a07af28317 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -136,6 +136,8 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.message.share.permalink.html'); $event->preload('wcf.message.share.socialMedia'); + $event->preload('wcf.message.quote.showQuotes'); + $event->preload('wcf.moderation.report.reportContent'); $event->preload('wcf.page.jumpTo'); From 1219a2410d724a8f6a78aeb8dc2254a12e8c1029 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 17 Dec 2024 13:55:00 +0100 Subject: [PATCH 03/65] Don't accept template to render the quote container content --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 16 ++++++---------- .../js/WoltLabSuite/Core/Component/Quote/List.js | 15 ++++++--------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 27604f26b6..2af4d50bc0 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -28,21 +28,17 @@ class QuoteList { throw new Error(`The quotes container for '${editorId}' does not exist.`); } - window.addEventListener("storage", (event) => { - if (event.key !== STORAGE_KEY) { - return; - } - - this.renderQuotes(event.newValue); + window.addEventListener("storage", () => { + this.renderQuotes(); }); - this.renderQuotes(window.localStorage.getItem(STORAGE_KEY)); + this.renderQuotes(); } - public renderQuotes(template: string | null): void { - this.#container.innerHTML = template || ""; + public renderQuotes(): void { + this.#container.innerHTML = window.localStorage.getItem(STORAGE_KEY) || ""; - if (template) { + if (this.#container.hasChildNodes()) { getTabMenu(this.#editorId)?.showTab( "quotes", getPhrase("wcf.message.quote.showQuotes", { 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 c00ea38c31..39105e436a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -18,17 +18,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C if (this.#container === null) { throw new Error(`The quotes container for '${editorId}' does not exist.`); } - window.addEventListener("storage", (event) => { - if (event.key !== exports.STORAGE_KEY) { - return; - } - this.renderQuotes(event.newValue); + window.addEventListener("storage", () => { + this.renderQuotes(); }); - this.renderQuotes(window.localStorage.getItem(exports.STORAGE_KEY)); + this.renderQuotes(); } - renderQuotes(template) { - this.#container.innerHTML = template || ""; - if (template) { + renderQuotes() { + this.#container.innerHTML = window.localStorage.getItem(exports.STORAGE_KEY) || ""; + if (this.#container.hasChildNodes()) { (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { count: this.#container.childElementCount, })); From 7e636aafcddc3fc9c8b20f3c518ce5bda883a5cd Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 18 Dec 2024 13:01:16 +0100 Subject: [PATCH 04/65] Mark `WCF.Message.Quote.Manager` as deprecated --- wcfsetup/install/files/js/WCF.Message.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js index db2204b95f..f369292f09 100644 --- a/wcfsetup/install/files/js/WCF.Message.js +++ b/wcfsetup/install/files/js/WCF.Message.js @@ -1062,6 +1062,8 @@ if (COMPILER_TARGET_DEFAULT) { * Manages stored quotes. * * @param integer count + * + * @deprecated 6.2 */ WCF.Message.Quote.Manager = Class.extend({ /** From 5f65a248074c63aa7f9ee4548cff6d8dbae0abf4 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 18 Dec 2024 13:09:49 +0100 Subject: [PATCH 05/65] Don't create new `WCF.Message.Quote.Manager` --- .../templates/shared_messageQuoteManager.tpl | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl b/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl index e8c9c7b0da..2460636c22 100644 --- a/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl +++ b/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl @@ -1,14 +1 @@ -WCF.Language.addObject({ - 'wcf.message.quote.insertAllQuotes': '{jslang}wcf.message.quote.insertAllQuotes{/jslang}', - 'wcf.message.quote.insertSelectedQuotes': '{jslang}wcf.message.quote.insertSelectedQuotes{/jslang}', - 'wcf.message.quote.manageQuotes': '{jslang}wcf.message.quote.manageQuotes{/jslang}', - 'wcf.message.quote.quoteSelected': '{jslang}wcf.message.quote.quoteSelected{/jslang}', - 'wcf.message.quote.quoteAndReply': '{jslang}wcf.message.quote.quoteAndReply{/jslang}', - 'wcf.message.quote.removeAllQuotes': '{jslang}wcf.message.quote.removeAllQuotes{/jslang}', - 'wcf.message.quote.removeSelectedQuotes': '{jslang}wcf.message.quote.removeSelectedQuotes{/jslang}', - 'wcf.message.quote.showQuotes': '{jslang __literal=true}wcf.message.quote.showQuotes{/jslang}' -}); - -{if !$wysiwygSelector|isset}{assign var=wysiwygSelector value=''}{/if} -{if !$supportPaste|isset}{assign var=supportPaste value=false}{/if} -var $quoteManager = new WCF.Message.Quote.Manager({@$__quoteCount}, '{$wysiwygSelector|encodeJS}', {if $supportPaste}true{else}false{/if}, [ {implode from=$__quoteRemove item=quoteID}'{$quoteID}'{/implode} ]); +{* Deprecated since 6.2 *} From ef8ca5f2bd591337878a3fd0cd4cecae2b8d8ee8 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 18 Dec 2024 13:10:31 +0100 Subject: [PATCH 06/65] Preload some language phrases for quotes --- .../listener/PreloadPhrasesCollectingListener.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index a07af28317..8aaf4fe612 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -136,6 +136,12 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.message.share.permalink.html'); $event->preload('wcf.message.share.socialMedia'); + $event->preload('wcf.message.quote.insertAllQuotes'); + $event->preload('wcf.message.quote.insertSelectedQuotes'); + $event->preload('wcf.message.quote.manageQuotes'); + $event->preload('wcf.message.quote.quoteSelected'); + $event->preload('wcf.message.quote.quoteAndReply'); + $event->preload('wcf.message.quote.removeAllQuotes'); $event->preload('wcf.message.quote.showQuotes'); $event->preload('wcf.moderation.report.reportContent'); From a888cf9914fff7f3370af287e3204630bab66241 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 18 Dec 2024 13:52:45 +0100 Subject: [PATCH 07/65] Mark `WoltLabSuite/Core/Ui/Message/Quote` as deprecated --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 9 + .../Core/Component/Quote/Message.ts | 393 ++++++++++++ ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 558 +----------------- .../WoltLabSuite/Core/Component/Quote/List.js | 16 +- .../Core/Component/Quote/Message.js | 311 ++++++++++ .../js/WoltLabSuite/Core/Ui/Message/Quote.js | 429 +------------- wcfsetup/install/files/style/ui/tooltip.scss | 2 +- 7 files changed, 738 insertions(+), 980 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Component/Quote/Message.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 2af4d50bc0..a83d633b2c 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -5,12 +5,15 @@ * @copyright 2001-2024 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.2 + * @woltlabExcludeBundle tiny */ + import * as Core from "WoltLabSuite/Core/Core"; import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; +import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const quoteLists = new Map(); @@ -68,6 +71,12 @@ export function setup(editorId: string): void { listenToCkeditor(editor).ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); + + setActiveEditor(ckeditor, true); + } else { + setActiveEditor(ckeditor, false); } + + //TODO handle active editor changed }); } diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts new file mode 100644 index 0000000000..a8c2665057 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -0,0 +1,393 @@ +/** + * Handles quotes selection in messages. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import DomUtil from "WoltLabSuite/Core/Dom/Util"; +import { getPhrase } from "WoltLabSuite/Core/Language"; +import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; +import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment"; +import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; + +interface Container { + element: HTMLElement; + messageBodySelector: string; +} + +const containers = new Map(); +let activeMessageId = ""; +let message = ""; +let activeEditor: CKEditor | undefined = undefined; +let timerSelectionChange: number | undefined = undefined; +let isMouseDown = false; +let objectId: number | undefined = undefined; +const copyQuote = document.createElement("div"); + +export function registerContainer(containerSelector: string, messageBodySelector: string): void { + wheneverFirstSeen(containerSelector, (container: HTMLElement) => { + const id = DomUtil.identify(container); + containers.set(id, { + element: container, + messageBodySelector: messageBodySelector, + }); + + if (container.classList.contains("jsInvalidQuoteTarget")) { + return; + } + + container.addEventListener("mousedown", (event) => onMouseDown(event)); + container.classList.add("jsQuoteMessageContainer"); + + container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => { + //TODO + }); + }); +} + +export function setActiveEditor(editor: CKEditor, supportDirectInsert: boolean) { + copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert")!.hidden = !supportDirectInsert; + + activeEditor = editor; +} + +function setup() { + copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy"); + + const buttonSaveQuote = document.createElement("button"); + buttonSaveQuote.type = "button"; + buttonSaveQuote.classList.add("jsQuoteManagerStore"); + buttonSaveQuote.textContent = getPhrase("wcf.message.quote.quoteSelected"); + buttonSaveQuote.addEventListener("click", () => { + //TODO + }); + copyQuote.appendChild(buttonSaveQuote); + const buttonSaveAndInsertQuote = document.createElement("button"); + buttonSaveAndInsertQuote.type = "button"; + buttonSaveAndInsertQuote.hidden = true; + buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); + buttonSaveAndInsertQuote.textContent = getPhrase("wcf.message.quote.quoteAndReply"); + buttonSaveAndInsertQuote.addEventListener("click", () => { + //TODO + }); + copyQuote.appendChild(buttonSaveAndInsertQuote); + + document.body.appendChild(copyQuote); + + document.addEventListener("mouseup", (event) => onMouseUp(event)); + document.addEventListener("selectionchange", () => onSelectionchange()); + + // Prevent the tooltip from being selectable while the touch pointer is being moved. + document.addEventListener( + "touchstart", + (event) => { + const target = event.target as HTMLElement; + if (target !== copyQuote && !copyQuote.contains(target)) { + copyQuote.classList.add("touchForceInaccessible"); + + document.addEventListener( + "touchend", + () => { + copyQuote.classList.remove("touchForceInaccessible"); + }, + { once: true, passive: false }, + ); + } + }, + { passive: false }, + ); + + window.addEventListener( + "resize", + () => { + copyQuote.classList.remove("active"); + }, + { passive: true }, + ); +} + +setup(); + +function getSelectedText(): string { + const selection = window.getSelection()!; + if (selection.rangeCount) { + return getNodeText(selection.getRangeAt(0).cloneContents()); + } + + return ""; +} + +/** + * Returns the text of a node and its children. + */ +function getNodeText(node: Node): string { + const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, { + acceptNode(node: Node): number { + if (node.nodeName === "BLOCKQUOTE" || node.nodeName === "SCRIPT") { + return NodeFilter.FILTER_REJECT; + } + + if (node instanceof HTMLImageElement) { + // Skip any image that is not a smiley or contains no alt text. + if (!node.classList.contains("smiley") || !node.alt) { + return NodeFilter.FILTER_REJECT; + } + } + + return NodeFilter.FILTER_ACCEPT; + }, + }); + + let text = ""; + const ignoreLinks: HTMLAnchorElement[] = []; + while (treeWalker.nextNode()) { + const node = treeWalker.currentNode as HTMLElement | Text; + + if (node instanceof Text) { + const parent = node.parentElement!; + if (parent instanceof HTMLAnchorElement && ignoreLinks.includes(parent)) { + // ignore text content of links that have already been captured + continue; + } + + // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing + // pointless linebreaks to be inserted. Replacing them with a simple space will + // preserve the spacing between words that would otherwise be lost. + text += node.nodeValue!.replace(/\n/g, " "); + + continue; + } + + if (node instanceof HTMLAnchorElement) { + // \u2026 === … + const value = node.textContent!; + if (value.indexOf("\u2026") > 0) { + const tmp = value.split(/\u2026/); + if (tmp.length === 2) { + const href = node.href; + if (href.indexOf(tmp[0]) === 0 && href.substring(tmp[1].length * -1) === tmp[1]) { + // This is a truncated url, use the original href instead to preserve the link. + text += href; + ignoreLinks.push(node); + } + } + } + } + + switch (node.nodeName) { + case "BR": + case "LI": + case "TD": + case "UL": + text += "\n"; + break; + + case "P": + text += "\n\n"; + break; + + // smilies + case "IMG": { + const img = node as HTMLImageElement; + text += ` ${img.alt} `; + break; + } + + // Code listing + case "DIV": + if (node.classList.contains("codeBoxHeadline") || node.classList.contains("codeBoxLine")) { + text += "\n"; + } + break; + } + } + + return text; +} + +function normalizeTextForComparison(text: string): string { + return text + .replace(/\r?\n|\r/g, "\n") + .replace(/\s/g, " ") + .replace(/\s{2,}/g, " "); +} + +function onSelectionchange(): void { + if (isMouseDown) { + return; + } + + if (activeMessageId === "") { + // check if the selection is non-empty and is entirely contained + // inside a single message container that is registered for quoting + const selection = window.getSelection()!; + if (selection.rangeCount !== 1 || selection.isCollapsed) { + return; + } + + const range = selection.getRangeAt(0); + const startContainer = DomUtil.closest(range.startContainer, ".jsQuoteMessageContainer"); + const endContainer = DomUtil.closest(range.endContainer, ".jsQuoteMessageContainer"); + if ( + startContainer && + startContainer === endContainer && + !startContainer.classList.contains("jsInvalidQuoteTarget") + ) { + // Check if the selection is visible, such as text marked inside containers with an + // active overflow handling attached to it. This can be a side effect of the browser + // search which modifies the text selection, but cannot be distinguished from manual + // selections initiated by the user. + let commonAncestor = range.commonAncestorContainer as HTMLElement; + if (commonAncestor.nodeType !== Node.ELEMENT_NODE) { + commonAncestor = commonAncestor.parentElement!; + } + + const offsetParent = commonAncestor.offsetParent!; + if (startContainer.contains(offsetParent)) { + if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) { + // The selected text is not visible to the user. + return; + } + } + + activeMessageId = startContainer.id; + } + } + + if (timerSelectionChange) { + window.clearTimeout(timerSelectionChange); + } + + timerSelectionChange = window.setTimeout(() => onMouseUp(), 100); +} + +function onMouseDown(event: MouseEvent): void { + // hide copy quote + copyQuote.classList.remove("active"); + + const message = event.currentTarget as HTMLElement; + activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id; + + if (timerSelectionChange) { + window.clearTimeout(timerSelectionChange); + timerSelectionChange = undefined; + } + + isMouseDown = true; +} + +function onMouseUp(event?: MouseEvent): void { + if (event instanceof Event) { + if (timerSelectionChange) { + // Prevent collisions of the `selectionchange` and the `mouseup` event. + window.clearTimeout(timerSelectionChange); + timerSelectionChange = undefined; + } + + isMouseDown = false; + } + + // ignore event + if (activeMessageId === "") { + copyQuote.classList.remove("active"); + + return; + } + + const selection = window.getSelection()!; + if (selection.rangeCount !== 1 || selection.isCollapsed) { + copyQuote.classList.remove("active"); + + return; + } + + const container = containers.get(activeMessageId); + if (container === undefined) { + // Since 5.4 we listen for global mouse events, because those are much + // more reliable on mobile devices. However, this can cause conflicts + // if two or more types of message types with quote support coexist on + // the same page. + return; + } + + const content = container.messageBodySelector + ? (container.element.querySelector(container.messageBodySelector) as HTMLElement) + : container; + + let anchorNode = selection.anchorNode; + while (anchorNode) { + if (anchorNode === content) { + break; + } + + anchorNode = anchorNode.parentNode; + } + + // selection spans unrelated nodes + if (anchorNode !== content) { + copyQuote.classList.remove("active"); + + return; + } + + const selectedText = getSelectedText(); + const text = selectedText.trim(); + if (text === "") { + copyQuote.classList.remove("active"); + + return; + } + + // check if mousedown/mouseup took place inside a blockquote + const range = selection.getRangeAt(0); + const startContainer = DomUtil.getClosestElement(range.startContainer); + const endContainer = DomUtil.getClosestElement(range.endContainer); + if (startContainer.closest("blockquote") || endContainer.closest("blockquote")) { + copyQuote.classList.remove("active"); + + return; + } + + // compare selection with message text of given container + const messageText = getNodeText(content); + + // selected text is not part of $messageText or contains text from unrelated nodes + if (!normalizeTextForComparison(messageText).includes(normalizeTextForComparison(text))) { + return; + } + + copyQuote.classList.add("active"); + const wasInaccessible = copyQuote.classList.contains("touchForceInaccessible"); + if (wasInaccessible) { + copyQuote.classList.remove("touchForceInaccessible"); + } + + setAlignment(copyQuote, endContainer); + + copyQuote.classList.remove("active"); + if (wasInaccessible) { + copyQuote.classList.add("touchForceInaccessible"); + } + + if (!timerSelectionChange) { + // reset containerID + activeMessageId = ""; + } else { + window.clearTimeout(timerSelectionChange); + timerSelectionChange = undefined; + } + + // show element after a delay, to prevent display if text was unmarked again (clicking into marked text) + window.setTimeout(() => { + const text = getSelectedText().trim(); + if (text !== "") { + copyQuote.classList.add("active"); + message = text; + objectId = ~~container.element.dataset.objectId!; + } + }, 10); +} diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index 0487723d93..8a2ee18458 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -1,31 +1,10 @@ /** * @woltlabExcludeBundle tiny + * + * @deprecated 6.2 use `WoltLabSuite/Core/Component/Quote/Message` instead */ -import * as Ajax from "../../Ajax"; -import * as Core from "../../Core"; -import * as EventHandler from "../../Event/Handler"; -import * as Language from "../../Language"; -import DomChangeListener from "../../Dom/Change/Listener"; -import DomUtil from "../../Dom/Util"; -import { AjaxCallbackObject, AjaxCallbackSetup } from "../../Ajax/Data"; - -interface AjaxResponse { - actionName: string; - returnValues: { - count?: number; - fullQuoteMessageIDs?: unknown; - fullQuoteObjectIDs?: unknown; - renderedQuote?: string; - }; -} - -interface ElementBoundaries { - bottom: number; - left: number; - right: number; - top: number; -} +import { registerContainer } from "WoltLabSuite/Core/Component/Quote/Message"; // see WCF.Message.Quote.Manager export interface WCFMessageQuoteManager { @@ -33,31 +12,7 @@ export interface WCFMessageQuoteManager { updateCount: (number, object) => void; } -export class UiMessageQuote implements AjaxCallbackObject { - private activeMessageId = ""; - - private readonly className: string; - - private containers = new Map(); - - private containerSelector = ""; - - private readonly copyQuote = document.createElement("div"); - - private message = ""; - - private readonly messageBodySelector: string; - - private objectId = 0; - - private objectType = ""; - - private timerSelectionChange?: number = undefined; - - private isMouseDown = false; - - private readonly quoteManager: WCFMessageQuoteManager; - +export class UiMessageQuote { /** * Initializes the quote handler for given object type. */ @@ -70,510 +25,7 @@ export class UiMessageQuote implements AjaxCallbackObject { messageContentSelector: string, supportDirectInsert: boolean, ) { - this.className = className; - this.objectType = objectType; - this.containerSelector = containerSelector; - this.messageBodySelector = messageBodySelector; - - this.initContainers(); - - supportDirectInsert = supportDirectInsert && quoteManager.supportPaste(); - this.quoteManager = quoteManager; - this.initCopyQuote(supportDirectInsert); - - document.addEventListener("mouseup", (event) => this.onMouseUp(event)); - document.addEventListener("selectionchange", () => this.onSelectionchange()); - - DomChangeListener.add("UiMessageQuote", () => this.initContainers()); - - // Prevent the tooltip from being selectable while the touch pointer is being moved. - document.addEventListener( - "touchstart", - (event) => { - const target = event.target as HTMLElement; - if (target !== this.copyQuote && !this.copyQuote.contains(target)) { - this.copyQuote.classList.add("touchForceInaccessible"); - - document.addEventListener( - "touchend", - () => { - this.copyQuote.classList.remove("touchForceInaccessible"); - }, - { once: true, passive: false }, - ); - } - }, - { passive: false }, - ); - - window.addEventListener( - "resize", - () => { - this.copyQuote.classList.remove("active"); - }, - { passive: true }, - ); - } - - /** - * Initializes message containers. - */ - private initContainers(): void { - document.querySelectorAll(this.containerSelector).forEach((container: HTMLElement) => { - const id = DomUtil.identify(container); - if (this.containers.has(id)) { - return; - } - - this.containers.set(id, container); - if (container.classList.contains("jsInvalidQuoteTarget")) { - return; - } - - container.addEventListener("mousedown", (event) => this.onMouseDown(event)); - container.classList.add("jsQuoteMessageContainer"); - - container - .querySelector(".jsQuoteMessage") - ?.addEventListener("click", (event: MouseEvent) => this.saveFullQuote(event)); - }); - } - - private onSelectionchange(): void { - if (this.isMouseDown) { - return; - } - - if (this.activeMessageId === "") { - // check if the selection is non-empty and is entirely contained - // inside a single message container that is registered for quoting - const selection = window.getSelection()!; - if (selection.rangeCount !== 1 || selection.isCollapsed) { - return; - } - - const range = selection.getRangeAt(0); - const startContainer = DomUtil.closest(range.startContainer, ".jsQuoteMessageContainer"); - const endContainer = DomUtil.closest(range.endContainer, ".jsQuoteMessageContainer"); - if ( - startContainer && - startContainer === endContainer && - !startContainer.classList.contains("jsInvalidQuoteTarget") - ) { - // Check if the selection is visible, such as text marked inside containers with an - // active overflow handling attached to it. This can be a side effect of the browser - // search which modifies the text selection, but cannot be distinguished from manual - // selections initiated by the user. - let commonAncestor = range.commonAncestorContainer as HTMLElement; - if (commonAncestor.nodeType !== Node.ELEMENT_NODE) { - commonAncestor = commonAncestor.parentElement!; - } - - const offsetParent = commonAncestor.offsetParent!; - if (startContainer.contains(offsetParent)) { - if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) { - // The selected text is not visible to the user. - return; - } - } - - this.activeMessageId = startContainer.id; - } - } - - if (this.timerSelectionChange) { - window.clearTimeout(this.timerSelectionChange); - } - - this.timerSelectionChange = window.setTimeout(() => this.onMouseUp(), 100); - } - - private onMouseDown(event: MouseEvent): void { - // hide copy quote - this.copyQuote.classList.remove("active"); - - const message = event.currentTarget as HTMLElement; - this.activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id; - - if (this.timerSelectionChange) { - window.clearTimeout(this.timerSelectionChange); - this.timerSelectionChange = undefined; - } - - this.isMouseDown = true; - } - - /** - * Returns the text of a node and its children. - */ - private getNodeText(node: Node): string { - const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, { - acceptNode(node: Node): number { - if (node.nodeName === "BLOCKQUOTE" || node.nodeName === "SCRIPT") { - return NodeFilter.FILTER_REJECT; - } - - if (node instanceof HTMLImageElement) { - // Skip any image that is not a smiley or contains no alt text. - if (!node.classList.contains("smiley") || !node.alt) { - return NodeFilter.FILTER_REJECT; - } - } - - return NodeFilter.FILTER_ACCEPT; - }, - }); - - let text = ""; - const ignoreLinks: HTMLAnchorElement[] = []; - while (treeWalker.nextNode()) { - const node = treeWalker.currentNode as HTMLElement | Text; - - if (node instanceof Text) { - const parent = node.parentElement!; - if (parent instanceof HTMLAnchorElement && ignoreLinks.includes(parent)) { - // ignore text content of links that have already been captured - continue; - } - - // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing - // pointless linebreaks to be inserted. Replacing them with a simple space will - // preserve the spacing between words that would otherwise be lost. - text += node.nodeValue!.replace(/\n/g, " "); - - continue; - } - - if (node instanceof HTMLAnchorElement) { - // \u2026 === … - const value = node.textContent!; - if (value.indexOf("\u2026") > 0) { - const tmp = value.split(/\u2026/); - if (tmp.length === 2) { - const href = node.href; - if (href.indexOf(tmp[0]) === 0 && href.substr(tmp[1].length * -1) === tmp[1]) { - // This is a truncated url, use the original href instead to preserve the link. - text += href; - ignoreLinks.push(node); - } - } - } - } - - switch (node.nodeName) { - case "BR": - case "LI": - case "TD": - case "UL": - text += "\n"; - break; - - case "P": - text += "\n\n"; - break; - - // smilies - case "IMG": { - const img = node as HTMLImageElement; - text += ` ${img.alt} `; - break; - } - - // Code listing - case "DIV": - if (node.classList.contains("codeBoxHeadline") || node.classList.contains("codeBoxLine")) { - text += "\n"; - } - break; - } - } - - return text; - } - - private onMouseUp(event?: MouseEvent): void { - if (event instanceof Event) { - if (this.timerSelectionChange) { - // Prevent collisions of the `selectionchange` and the `mouseup` event. - window.clearTimeout(this.timerSelectionChange); - this.timerSelectionChange = undefined; - } - - this.isMouseDown = false; - } - - // ignore event - if (this.activeMessageId === "") { - this.copyQuote.classList.remove("active"); - - return; - } - - const selection = window.getSelection()!; - if (selection.rangeCount !== 1 || selection.isCollapsed) { - this.copyQuote.classList.remove("active"); - - return; - } - - const container = this.containers.get(this.activeMessageId); - if (container === undefined) { - // Since 5.4 we listen for global mouse events, because those are much - // more reliable on mobile devices. However, this can cause conflicts - // if two or more types of message types with quote support coexist on - // the same page. - return; - } - - const objectId = ~~container.dataset.objectId!; - const content = this.messageBodySelector - ? (container.querySelector(this.messageBodySelector) as HTMLElement) - : container; - - let anchorNode = selection.anchorNode; - while (anchorNode) { - if (anchorNode === content) { - break; - } - - anchorNode = anchorNode.parentNode; - } - - // selection spans unrelated nodes - if (anchorNode !== content) { - this.copyQuote.classList.remove("active"); - - return; - } - - const selectedText = this.getSelectedText(); - const text = selectedText.trim(); - if (text === "") { - this.copyQuote.classList.remove("active"); - - return; - } - - // check if mousedown/mouseup took place inside a blockquote - const range = selection.getRangeAt(0); - const startContainer = DomUtil.getClosestElement(range.startContainer); - const endContainer = DomUtil.getClosestElement(range.endContainer); - if (startContainer.closest("blockquote") || endContainer.closest("blockquote")) { - this.copyQuote.classList.remove("active"); - - return; - } - - // compare selection with message text of given container - const messageText = this.getNodeText(content); - - // selected text is not part of $messageText or contains text from unrelated nodes - if (!this.normalizeTextForComparison(messageText).includes(this.normalizeTextForComparison(text))) { - return; - } - - this.copyQuote.classList.add("active"); - const wasInaccessible = this.copyQuote.classList.contains("touchForceInaccessible"); - if (wasInaccessible) { - this.copyQuote.classList.remove("touchForceInaccessible"); - } - - const coordinates = this.getElementBoundaries(selection)!; - const dimensions = { height: this.copyQuote.offsetHeight, width: this.copyQuote.offsetWidth }; - let left = (coordinates.right - coordinates.left) / 2 - dimensions.width / 2 + coordinates.left; - - // Prevent the overlay from overflowing the left or right boundary of the container. - const containerBoundaries = content.getBoundingClientRect(); - if (left < containerBoundaries.left) { - left = containerBoundaries.left; - } else if (left + dimensions.width > containerBoundaries.right) { - left = containerBoundaries.right - dimensions.width; - } - - this.copyQuote.style.setProperty("top", `${coordinates.bottom + 7}px`); - this.copyQuote.style.setProperty("left", `${left}px`); - this.copyQuote.classList.remove("active"); - if (wasInaccessible) { - this.copyQuote.classList.add("touchForceInaccessible"); - } - - if (!this.timerSelectionChange) { - // reset containerID - this.activeMessageId = ""; - } else { - window.clearTimeout(this.timerSelectionChange); - this.timerSelectionChange = undefined; - } - - // show element after a delay, to prevent display if text was unmarked again (clicking into marked text) - window.setTimeout(() => { - const text = this.getSelectedText().trim(); - if (text !== "") { - this.copyQuote.classList.add("active"); - this.message = text; - this.objectId = objectId; - } - }, 10); - } - - private normalizeTextForComparison(text: string): string { - return text - .replace(/\r?\n|\r/g, "\n") - .replace(/\s/g, " ") - .replace(/\s{2,}/g, " "); - } - - private getElementBoundaries(selection: Selection): ElementBoundaries | null { - let coordinates: ElementBoundaries | null = null; - - if (selection.rangeCount > 0) { - // The coordinates returned by getBoundingClientRect() are relative to the - // viewport, not the document. - const rect = selection.getRangeAt(0).getBoundingClientRect(); - - const scrollTop = window.pageYOffset; - coordinates = { - bottom: rect.bottom + scrollTop, - left: rect.left, - right: rect.right, - top: rect.top + scrollTop, - }; - } - - return coordinates; - } - - private initCopyQuote(supportDirectInsert: boolean): void { - this.copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy"); - - const buttonSaveQuote = document.createElement("span"); - buttonSaveQuote.classList.add("jsQuoteManagerStore"); - buttonSaveQuote.textContent = Language.get("wcf.message.quote.quoteSelected"); - buttonSaveQuote.addEventListener("click", (event) => this.saveQuote(event)); - this.copyQuote.appendChild(buttonSaveQuote); - - if (supportDirectInsert) { - const buttonSaveAndInsertQuote = document.createElement("span"); - buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); - buttonSaveAndInsertQuote.textContent = Language.get("wcf.message.quote.quoteAndReply"); - buttonSaveAndInsertQuote.addEventListener("click", (event) => this.saveAndInsertQuote(event)); - this.copyQuote.appendChild(buttonSaveAndInsertQuote); - } - - document.body.appendChild(this.copyQuote); - } - - private getSelectedText(): string { - const selection = window.getSelection()!; - if (selection.rangeCount) { - return this.getNodeText(selection.getRangeAt(0).cloneContents()); - } - - return ""; - } - - private saveFullQuote(event: MouseEvent): void { - event.preventDefault(); - - const listItem = event.currentTarget as HTMLElement; - - Ajax.api(this, { - actionName: "saveFullQuote", - objectIDs: [listItem.dataset.objectId], - }); - - // mark element as quoted - const quoteLink = listItem.querySelector("a")!; - if (Core.stringToBool(listItem.dataset.isQuoted || "")) { - listItem.dataset.isQuoted = "false"; - quoteLink.classList.remove("active"); - } else { - listItem.dataset.isQuoted = "true"; - quoteLink.classList.add("active"); - } - - // close navigation on mobile - const navigationList: HTMLUListElement | null = listItem.closest(".buttonGroupNavigation"); - if (navigationList && navigationList.classList.contains("jsMobileButtonGroupNavigation")) { - const dropDownLabel = navigationList.querySelector(".dropdownLabel") as HTMLElement; - dropDownLabel.click(); - } - } - - private saveQuote(event?: MouseEvent, renderQuote = false) { - event?.preventDefault(); - - Ajax.api(this, { - actionName: "saveQuote", - objectIDs: [this.objectId], - parameters: { - message: this.message, - renderQuote, - }, - }); - - const selection = window.getSelection()!; - if (selection.rangeCount) { - selection.removeAllRanges(); - this.copyQuote.classList.remove("active"); - } - } - - private saveAndInsertQuote(event: MouseEvent) { - event.preventDefault(); - - this.saveQuote(undefined, true); - } - - _ajaxSuccess(data: AjaxResponse): void { - if (data.returnValues.count !== undefined) { - if (data.returnValues.fullQuoteMessageIDs !== undefined) { - data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs; - } - - const fullQuoteObjectIDs = data.returnValues.fullQuoteObjectIDs || {}; - this.quoteManager.updateCount(data.returnValues.count, fullQuoteObjectIDs); - } - - switch (data.actionName) { - case "saveQuote": - case "saveFullQuote": - if (data.returnValues.renderedQuote) { - EventHandler.fire("com.woltlab.wcf.message.quote", "insert", { - forceInsert: data.actionName === "saveQuote", - quote: data.returnValues.renderedQuote, - }); - } - break; - } - } - - _ajaxSetup(): ReturnType { - return { - data: { - className: this.className, - interfaceName: "wcf\\data\\IMessageQuoteAction", - }, - }; - } - - /** - * Updates the full quote data for all matching objects. - */ - updateFullQuoteObjectIDs(objectIds: number[]): void { - this.containers.forEach((message) => { - const quoteButton = message.querySelector(".jsQuoteMessage") as HTMLLIElement; - quoteButton.dataset.isQuoted = "false"; - - const quoteButtonLink = quoteButton.querySelector("a")!; - quoteButton.classList.remove("active"); - - const objectId = ~~quoteButton.dataset.objectID!; - if (objectIds.includes(objectId)) { - quoteButton.dataset.isQuoted = "true"; - quoteButtonLink.classList.add("active"); - } - }); + registerContainer(containerSelector, messageBodySelector); } } 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 39105e436a..179fa611ef 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -1,4 +1,13 @@ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1) { +/** + * Handles quotes for CKEditor 5 message fields. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ +define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1, Message_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.STORAGE_KEY = void 0; @@ -49,7 +58,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C (0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); + (0, Message_1.setActiveEditor)(ckeditor, true); } + else { + (0, Message_1.setActiveEditor)(ckeditor, false); + } + //TODO handle active editor changed }); } }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js new file mode 100644 index 0000000000..3068f6aad6 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -0,0 +1,311 @@ +/** + * Handles quotes selection in messages. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ +define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.registerContainer = registerContainer; + exports.setActiveEditor = setActiveEditor; + Util_1 = tslib_1.__importDefault(Util_1); + const containers = new Map(); + let activeMessageId = ""; + let message = ""; + let activeEditor = undefined; + let timerSelectionChange = undefined; + let isMouseDown = false; + let objectId = undefined; + const copyQuote = document.createElement("div"); + function registerContainer(containerSelector, messageBodySelector) { + (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { + const id = Util_1.default.identify(container); + containers.set(id, { + element: container, + messageBodySelector: messageBodySelector, + }); + if (container.classList.contains("jsInvalidQuoteTarget")) { + return; + } + container.addEventListener("mousedown", (event) => onMouseDown(event)); + container.classList.add("jsQuoteMessageContainer"); + container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => { + //TODO + }); + }); + } + function setActiveEditor(editor, supportDirectInsert) { + copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert").hidden = !supportDirectInsert; + activeEditor = editor; + } + function setup() { + copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy"); + const buttonSaveQuote = document.createElement("button"); + buttonSaveQuote.type = "button"; + buttonSaveQuote.classList.add("jsQuoteManagerStore"); + buttonSaveQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteSelected"); + buttonSaveQuote.addEventListener("click", () => { + //TODO + }); + copyQuote.appendChild(buttonSaveQuote); + const buttonSaveAndInsertQuote = document.createElement("button"); + buttonSaveAndInsertQuote.type = "button"; + buttonSaveAndInsertQuote.hidden = true; + buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); + buttonSaveAndInsertQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteAndReply"); + buttonSaveAndInsertQuote.addEventListener("click", () => { + //TODO + }); + copyQuote.appendChild(buttonSaveAndInsertQuote); + document.body.appendChild(copyQuote); + document.addEventListener("mouseup", (event) => onMouseUp(event)); + document.addEventListener("selectionchange", () => onSelectionchange()); + // Prevent the tooltip from being selectable while the touch pointer is being moved. + document.addEventListener("touchstart", (event) => { + const target = event.target; + if (target !== copyQuote && !copyQuote.contains(target)) { + copyQuote.classList.add("touchForceInaccessible"); + document.addEventListener("touchend", () => { + copyQuote.classList.remove("touchForceInaccessible"); + }, { once: true, passive: false }); + } + }, { passive: false }); + window.addEventListener("resize", () => { + copyQuote.classList.remove("active"); + }, { passive: true }); + } + setup(); + function getSelectedText() { + const selection = window.getSelection(); + if (selection.rangeCount) { + return getNodeText(selection.getRangeAt(0).cloneContents()); + } + return ""; + } + /** + * Returns the text of a node and its children. + */ + function getNodeText(node) { + const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, { + acceptNode(node) { + if (node.nodeName === "BLOCKQUOTE" || node.nodeName === "SCRIPT") { + return NodeFilter.FILTER_REJECT; + } + if (node instanceof HTMLImageElement) { + // Skip any image that is not a smiley or contains no alt text. + if (!node.classList.contains("smiley") || !node.alt) { + return NodeFilter.FILTER_REJECT; + } + } + return NodeFilter.FILTER_ACCEPT; + }, + }); + let text = ""; + const ignoreLinks = []; + while (treeWalker.nextNode()) { + const node = treeWalker.currentNode; + if (node instanceof Text) { + const parent = node.parentElement; + if (parent instanceof HTMLAnchorElement && ignoreLinks.includes(parent)) { + // ignore text content of links that have already been captured + continue; + } + // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing + // pointless linebreaks to be inserted. Replacing them with a simple space will + // preserve the spacing between words that would otherwise be lost. + text += node.nodeValue.replace(/\n/g, " "); + continue; + } + if (node instanceof HTMLAnchorElement) { + // \u2026 === … + const value = node.textContent; + if (value.indexOf("\u2026") > 0) { + const tmp = value.split(/\u2026/); + if (tmp.length === 2) { + const href = node.href; + if (href.indexOf(tmp[0]) === 0 && href.substring(tmp[1].length * -1) === tmp[1]) { + // This is a truncated url, use the original href instead to preserve the link. + text += href; + ignoreLinks.push(node); + } + } + } + } + switch (node.nodeName) { + case "BR": + case "LI": + case "TD": + case "UL": + text += "\n"; + break; + case "P": + text += "\n\n"; + break; + // smilies + case "IMG": { + const img = node; + text += ` ${img.alt} `; + break; + } + // Code listing + case "DIV": + if (node.classList.contains("codeBoxHeadline") || node.classList.contains("codeBoxLine")) { + text += "\n"; + } + break; + } + } + return text; + } + function normalizeTextForComparison(text) { + return text + .replace(/\r?\n|\r/g, "\n") + .replace(/\s/g, " ") + .replace(/\s{2,}/g, " "); + } + function onSelectionchange() { + if (isMouseDown) { + return; + } + if (activeMessageId === "") { + // check if the selection is non-empty and is entirely contained + // inside a single message container that is registered for quoting + const selection = window.getSelection(); + if (selection.rangeCount !== 1 || selection.isCollapsed) { + return; + } + const range = selection.getRangeAt(0); + const startContainer = Util_1.default.closest(range.startContainer, ".jsQuoteMessageContainer"); + const endContainer = Util_1.default.closest(range.endContainer, ".jsQuoteMessageContainer"); + if (startContainer && + startContainer === endContainer && + !startContainer.classList.contains("jsInvalidQuoteTarget")) { + // Check if the selection is visible, such as text marked inside containers with an + // active overflow handling attached to it. This can be a side effect of the browser + // search which modifies the text selection, but cannot be distinguished from manual + // selections initiated by the user. + let commonAncestor = range.commonAncestorContainer; + if (commonAncestor.nodeType !== Node.ELEMENT_NODE) { + commonAncestor = commonAncestor.parentElement; + } + const offsetParent = commonAncestor.offsetParent; + if (startContainer.contains(offsetParent)) { + if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) { + // The selected text is not visible to the user. + return; + } + } + activeMessageId = startContainer.id; + } + } + if (timerSelectionChange) { + window.clearTimeout(timerSelectionChange); + } + timerSelectionChange = window.setTimeout(() => onMouseUp(), 100); + } + function onMouseDown(event) { + // hide copy quote + copyQuote.classList.remove("active"); + const message = event.currentTarget; + activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id; + if (timerSelectionChange) { + window.clearTimeout(timerSelectionChange); + timerSelectionChange = undefined; + } + isMouseDown = true; + } + function onMouseUp(event) { + if (event instanceof Event) { + if (timerSelectionChange) { + // Prevent collisions of the `selectionchange` and the `mouseup` event. + window.clearTimeout(timerSelectionChange); + timerSelectionChange = undefined; + } + isMouseDown = false; + } + // ignore event + if (activeMessageId === "") { + copyQuote.classList.remove("active"); + return; + } + const selection = window.getSelection(); + if (selection.rangeCount !== 1 || selection.isCollapsed) { + copyQuote.classList.remove("active"); + return; + } + const container = containers.get(activeMessageId); + if (container === undefined) { + // Since 5.4 we listen for global mouse events, because those are much + // more reliable on mobile devices. However, this can cause conflicts + // if two or more types of message types with quote support coexist on + // the same page. + return; + } + const content = container.messageBodySelector + ? container.element.querySelector(container.messageBodySelector) + : container; + let anchorNode = selection.anchorNode; + while (anchorNode) { + if (anchorNode === content) { + break; + } + anchorNode = anchorNode.parentNode; + } + // selection spans unrelated nodes + if (anchorNode !== content) { + copyQuote.classList.remove("active"); + return; + } + const selectedText = getSelectedText(); + const text = selectedText.trim(); + if (text === "") { + copyQuote.classList.remove("active"); + return; + } + // check if mousedown/mouseup took place inside a blockquote + const range = selection.getRangeAt(0); + const startContainer = Util_1.default.getClosestElement(range.startContainer); + const endContainer = Util_1.default.getClosestElement(range.endContainer); + if (startContainer.closest("blockquote") || endContainer.closest("blockquote")) { + copyQuote.classList.remove("active"); + return; + } + // compare selection with message text of given container + const messageText = getNodeText(content); + // selected text is not part of $messageText or contains text from unrelated nodes + if (!normalizeTextForComparison(messageText).includes(normalizeTextForComparison(text))) { + return; + } + copyQuote.classList.add("active"); + const wasInaccessible = copyQuote.classList.contains("touchForceInaccessible"); + if (wasInaccessible) { + copyQuote.classList.remove("touchForceInaccessible"); + } + (0, Alignment_1.set)(copyQuote, endContainer); + copyQuote.classList.remove("active"); + if (wasInaccessible) { + copyQuote.classList.add("touchForceInaccessible"); + } + if (!timerSelectionChange) { + // reset containerID + activeMessageId = ""; + } + else { + window.clearTimeout(timerSelectionChange); + timerSelectionChange = undefined; + } + // show element after a delay, to prevent display if text was unmarked again (clicking into marked text) + window.setTimeout(() => { + const text = getSelectedText().trim(); + if (text !== "") { + copyQuote.classList.add("active"); + message = text; + objectId = ~~container.element.dataset.objectId; + } + }, 10); + } +}); 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 b094092560..fa5784e730 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -1,439 +1,18 @@ /** * @woltlabExcludeBundle tiny + * + * @deprecated 6.2 use `WoltLabSuite/Core/Component/Quote/Message` instead */ -define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1) { +define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, Message_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UiMessageQuote = void 0; - Ajax = tslib_1.__importStar(Ajax); - Core = tslib_1.__importStar(Core); - EventHandler = tslib_1.__importStar(EventHandler); - Language = tslib_1.__importStar(Language); - Listener_1 = tslib_1.__importDefault(Listener_1); - Util_1 = tslib_1.__importDefault(Util_1); class UiMessageQuote { - activeMessageId = ""; - className; - containers = new Map(); - containerSelector = ""; - copyQuote = document.createElement("div"); - message = ""; - messageBodySelector; - objectId = 0; - objectType = ""; - timerSelectionChange = undefined; - isMouseDown = false; - quoteManager; /** * Initializes the quote handler for given object type. */ constructor(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) { - this.className = className; - this.objectType = objectType; - this.containerSelector = containerSelector; - this.messageBodySelector = messageBodySelector; - this.initContainers(); - supportDirectInsert = supportDirectInsert && quoteManager.supportPaste(); - this.quoteManager = quoteManager; - this.initCopyQuote(supportDirectInsert); - document.addEventListener("mouseup", (event) => this.onMouseUp(event)); - document.addEventListener("selectionchange", () => this.onSelectionchange()); - Listener_1.default.add("UiMessageQuote", () => this.initContainers()); - // Prevent the tooltip from being selectable while the touch pointer is being moved. - document.addEventListener("touchstart", (event) => { - const target = event.target; - if (target !== this.copyQuote && !this.copyQuote.contains(target)) { - this.copyQuote.classList.add("touchForceInaccessible"); - document.addEventListener("touchend", () => { - this.copyQuote.classList.remove("touchForceInaccessible"); - }, { once: true, passive: false }); - } - }, { passive: false }); - window.addEventListener("resize", () => { - this.copyQuote.classList.remove("active"); - }, { passive: true }); - } - /** - * Initializes message containers. - */ - initContainers() { - document.querySelectorAll(this.containerSelector).forEach((container) => { - const id = Util_1.default.identify(container); - if (this.containers.has(id)) { - return; - } - this.containers.set(id, container); - if (container.classList.contains("jsInvalidQuoteTarget")) { - return; - } - container.addEventListener("mousedown", (event) => this.onMouseDown(event)); - container.classList.add("jsQuoteMessageContainer"); - container - .querySelector(".jsQuoteMessage") - ?.addEventListener("click", (event) => this.saveFullQuote(event)); - }); - } - onSelectionchange() { - if (this.isMouseDown) { - return; - } - if (this.activeMessageId === "") { - // check if the selection is non-empty and is entirely contained - // inside a single message container that is registered for quoting - const selection = window.getSelection(); - if (selection.rangeCount !== 1 || selection.isCollapsed) { - return; - } - const range = selection.getRangeAt(0); - const startContainer = Util_1.default.closest(range.startContainer, ".jsQuoteMessageContainer"); - const endContainer = Util_1.default.closest(range.endContainer, ".jsQuoteMessageContainer"); - if (startContainer && - startContainer === endContainer && - !startContainer.classList.contains("jsInvalidQuoteTarget")) { - // Check if the selection is visible, such as text marked inside containers with an - // active overflow handling attached to it. This can be a side effect of the browser - // search which modifies the text selection, but cannot be distinguished from manual - // selections initiated by the user. - let commonAncestor = range.commonAncestorContainer; - if (commonAncestor.nodeType !== Node.ELEMENT_NODE) { - commonAncestor = commonAncestor.parentElement; - } - const offsetParent = commonAncestor.offsetParent; - if (startContainer.contains(offsetParent)) { - if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) { - // The selected text is not visible to the user. - return; - } - } - this.activeMessageId = startContainer.id; - } - } - if (this.timerSelectionChange) { - window.clearTimeout(this.timerSelectionChange); - } - this.timerSelectionChange = window.setTimeout(() => this.onMouseUp(), 100); - } - onMouseDown(event) { - // hide copy quote - this.copyQuote.classList.remove("active"); - const message = event.currentTarget; - this.activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id; - if (this.timerSelectionChange) { - window.clearTimeout(this.timerSelectionChange); - this.timerSelectionChange = undefined; - } - this.isMouseDown = true; - } - /** - * Returns the text of a node and its children. - */ - getNodeText(node) { - const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, { - acceptNode(node) { - if (node.nodeName === "BLOCKQUOTE" || node.nodeName === "SCRIPT") { - return NodeFilter.FILTER_REJECT; - } - if (node instanceof HTMLImageElement) { - // Skip any image that is not a smiley or contains no alt text. - if (!node.classList.contains("smiley") || !node.alt) { - return NodeFilter.FILTER_REJECT; - } - } - return NodeFilter.FILTER_ACCEPT; - }, - }); - let text = ""; - const ignoreLinks = []; - while (treeWalker.nextNode()) { - const node = treeWalker.currentNode; - if (node instanceof Text) { - const parent = node.parentElement; - if (parent instanceof HTMLAnchorElement && ignoreLinks.includes(parent)) { - // ignore text content of links that have already been captured - continue; - } - // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing - // pointless linebreaks to be inserted. Replacing them with a simple space will - // preserve the spacing between words that would otherwise be lost. - text += node.nodeValue.replace(/\n/g, " "); - continue; - } - if (node instanceof HTMLAnchorElement) { - // \u2026 === … - const value = node.textContent; - if (value.indexOf("\u2026") > 0) { - const tmp = value.split(/\u2026/); - if (tmp.length === 2) { - const href = node.href; - if (href.indexOf(tmp[0]) === 0 && href.substr(tmp[1].length * -1) === tmp[1]) { - // This is a truncated url, use the original href instead to preserve the link. - text += href; - ignoreLinks.push(node); - } - } - } - } - switch (node.nodeName) { - case "BR": - case "LI": - case "TD": - case "UL": - text += "\n"; - break; - case "P": - text += "\n\n"; - break; - // smilies - case "IMG": { - const img = node; - text += ` ${img.alt} `; - break; - } - // Code listing - case "DIV": - if (node.classList.contains("codeBoxHeadline") || node.classList.contains("codeBoxLine")) { - text += "\n"; - } - break; - } - } - return text; - } - onMouseUp(event) { - if (event instanceof Event) { - if (this.timerSelectionChange) { - // Prevent collisions of the `selectionchange` and the `mouseup` event. - window.clearTimeout(this.timerSelectionChange); - this.timerSelectionChange = undefined; - } - this.isMouseDown = false; - } - // ignore event - if (this.activeMessageId === "") { - this.copyQuote.classList.remove("active"); - return; - } - const selection = window.getSelection(); - if (selection.rangeCount !== 1 || selection.isCollapsed) { - this.copyQuote.classList.remove("active"); - return; - } - const container = this.containers.get(this.activeMessageId); - if (container === undefined) { - // Since 5.4 we listen for global mouse events, because those are much - // more reliable on mobile devices. However, this can cause conflicts - // if two or more types of message types with quote support coexist on - // the same page. - return; - } - const objectId = ~~container.dataset.objectId; - const content = this.messageBodySelector - ? container.querySelector(this.messageBodySelector) - : container; - let anchorNode = selection.anchorNode; - while (anchorNode) { - if (anchorNode === content) { - break; - } - anchorNode = anchorNode.parentNode; - } - // selection spans unrelated nodes - if (anchorNode !== content) { - this.copyQuote.classList.remove("active"); - return; - } - const selectedText = this.getSelectedText(); - const text = selectedText.trim(); - if (text === "") { - this.copyQuote.classList.remove("active"); - return; - } - // check if mousedown/mouseup took place inside a blockquote - const range = selection.getRangeAt(0); - const startContainer = Util_1.default.getClosestElement(range.startContainer); - const endContainer = Util_1.default.getClosestElement(range.endContainer); - if (startContainer.closest("blockquote") || endContainer.closest("blockquote")) { - this.copyQuote.classList.remove("active"); - return; - } - // compare selection with message text of given container - const messageText = this.getNodeText(content); - // selected text is not part of $messageText or contains text from unrelated nodes - if (!this.normalizeTextForComparison(messageText).includes(this.normalizeTextForComparison(text))) { - return; - } - this.copyQuote.classList.add("active"); - const wasInaccessible = this.copyQuote.classList.contains("touchForceInaccessible"); - if (wasInaccessible) { - this.copyQuote.classList.remove("touchForceInaccessible"); - } - const coordinates = this.getElementBoundaries(selection); - const dimensions = { height: this.copyQuote.offsetHeight, width: this.copyQuote.offsetWidth }; - let left = (coordinates.right - coordinates.left) / 2 - dimensions.width / 2 + coordinates.left; - // Prevent the overlay from overflowing the left or right boundary of the container. - const containerBoundaries = content.getBoundingClientRect(); - if (left < containerBoundaries.left) { - left = containerBoundaries.left; - } - else if (left + dimensions.width > containerBoundaries.right) { - left = containerBoundaries.right - dimensions.width; - } - this.copyQuote.style.setProperty("top", `${coordinates.bottom + 7}px`); - this.copyQuote.style.setProperty("left", `${left}px`); - this.copyQuote.classList.remove("active"); - if (wasInaccessible) { - this.copyQuote.classList.add("touchForceInaccessible"); - } - if (!this.timerSelectionChange) { - // reset containerID - this.activeMessageId = ""; - } - else { - window.clearTimeout(this.timerSelectionChange); - this.timerSelectionChange = undefined; - } - // show element after a delay, to prevent display if text was unmarked again (clicking into marked text) - window.setTimeout(() => { - const text = this.getSelectedText().trim(); - if (text !== "") { - this.copyQuote.classList.add("active"); - this.message = text; - this.objectId = objectId; - } - }, 10); - } - normalizeTextForComparison(text) { - return text - .replace(/\r?\n|\r/g, "\n") - .replace(/\s/g, " ") - .replace(/\s{2,}/g, " "); - } - getElementBoundaries(selection) { - let coordinates = null; - if (selection.rangeCount > 0) { - // The coordinates returned by getBoundingClientRect() are relative to the - // viewport, not the document. - const rect = selection.getRangeAt(0).getBoundingClientRect(); - const scrollTop = window.pageYOffset; - coordinates = { - bottom: rect.bottom + scrollTop, - left: rect.left, - right: rect.right, - top: rect.top + scrollTop, - }; - } - return coordinates; - } - initCopyQuote(supportDirectInsert) { - this.copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy"); - const buttonSaveQuote = document.createElement("span"); - buttonSaveQuote.classList.add("jsQuoteManagerStore"); - buttonSaveQuote.textContent = Language.get("wcf.message.quote.quoteSelected"); - buttonSaveQuote.addEventListener("click", (event) => this.saveQuote(event)); - this.copyQuote.appendChild(buttonSaveQuote); - if (supportDirectInsert) { - const buttonSaveAndInsertQuote = document.createElement("span"); - buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); - buttonSaveAndInsertQuote.textContent = Language.get("wcf.message.quote.quoteAndReply"); - buttonSaveAndInsertQuote.addEventListener("click", (event) => this.saveAndInsertQuote(event)); - this.copyQuote.appendChild(buttonSaveAndInsertQuote); - } - document.body.appendChild(this.copyQuote); - } - getSelectedText() { - const selection = window.getSelection(); - if (selection.rangeCount) { - return this.getNodeText(selection.getRangeAt(0).cloneContents()); - } - return ""; - } - saveFullQuote(event) { - event.preventDefault(); - const listItem = event.currentTarget; - Ajax.api(this, { - actionName: "saveFullQuote", - objectIDs: [listItem.dataset.objectId], - }); - // mark element as quoted - const quoteLink = listItem.querySelector("a"); - if (Core.stringToBool(listItem.dataset.isQuoted || "")) { - listItem.dataset.isQuoted = "false"; - quoteLink.classList.remove("active"); - } - else { - listItem.dataset.isQuoted = "true"; - quoteLink.classList.add("active"); - } - // close navigation on mobile - const navigationList = listItem.closest(".buttonGroupNavigation"); - if (navigationList && navigationList.classList.contains("jsMobileButtonGroupNavigation")) { - const dropDownLabel = navigationList.querySelector(".dropdownLabel"); - dropDownLabel.click(); - } - } - saveQuote(event, renderQuote = false) { - event?.preventDefault(); - Ajax.api(this, { - actionName: "saveQuote", - objectIDs: [this.objectId], - parameters: { - message: this.message, - renderQuote, - }, - }); - const selection = window.getSelection(); - if (selection.rangeCount) { - selection.removeAllRanges(); - this.copyQuote.classList.remove("active"); - } - } - saveAndInsertQuote(event) { - event.preventDefault(); - this.saveQuote(undefined, true); - } - _ajaxSuccess(data) { - if (data.returnValues.count !== undefined) { - if (data.returnValues.fullQuoteMessageIDs !== undefined) { - data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs; - } - const fullQuoteObjectIDs = data.returnValues.fullQuoteObjectIDs || {}; - this.quoteManager.updateCount(data.returnValues.count, fullQuoteObjectIDs); - } - switch (data.actionName) { - case "saveQuote": - case "saveFullQuote": - if (data.returnValues.renderedQuote) { - EventHandler.fire("com.woltlab.wcf.message.quote", "insert", { - forceInsert: data.actionName === "saveQuote", - quote: data.returnValues.renderedQuote, - }); - } - break; - } - } - _ajaxSetup() { - return { - data: { - className: this.className, - interfaceName: "wcf\\data\\IMessageQuoteAction", - }, - }; - } - /** - * Updates the full quote data for all matching objects. - */ - updateFullQuoteObjectIDs(objectIds) { - this.containers.forEach((message) => { - const quoteButton = message.querySelector(".jsQuoteMessage"); - quoteButton.dataset.isQuoted = "false"; - const quoteButtonLink = quoteButton.querySelector("a"); - quoteButton.classList.remove("active"); - const objectId = ~~quoteButton.dataset.objectID; - if (objectIds.includes(objectId)) { - quoteButton.dataset.isQuoted = "true"; - quoteButtonLink.classList.add("active"); - } - }); + (0, Message_1.registerContainer)(containerSelector, messageBodySelector); } } exports.UiMessageQuote = UiMessageQuote; diff --git a/wcfsetup/install/files/style/ui/tooltip.scss b/wcfsetup/install/files/style/ui/tooltip.scss index 202c3db064..ef2ccf7a1a 100644 --- a/wcfsetup/install/files/style/ui/tooltip.scss +++ b/wcfsetup/install/files/style/ui/tooltip.scss @@ -26,7 +26,7 @@ &.interactive { pointer-events: all; - > span { + > span, button { cursor: pointer; &:not(:first-child) { From 1e70d82fd2fb19eb4cba86723f17795e3a45b62e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 08:54:46 +0100 Subject: [PATCH 08/65] Add a function to get the `focusTracker` from the ckeditor --- ts/WoltLabSuite/Core/Component/Ckeditor.ts | 4 ++++ .../install/files/js/WoltLabSuite/Core/Component/Ckeditor.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Ckeditor.ts b/ts/WoltLabSuite/Core/Component/Ckeditor.ts index 06bfe16aff..efa2dda198 100644 --- a/ts/WoltLabSuite/Core/Component/Ckeditor.ts +++ b/ts/WoltLabSuite/Core/Component/Ckeditor.ts @@ -163,6 +163,10 @@ class Ckeditor { get sourceElement(): HTMLElement { return this.#editor.sourceElement!; } + + get focusTracker(): CKEditor5.Utils.FocusTracker { + return this.#editor.ui.focusTracker; + } } function* findModelForRemoval( diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor.js index 45c91c58c9..092f913af9 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor.js @@ -125,6 +125,9 @@ define(["require", "exports", "tslib", "./Ckeditor/Attachment", "./Ckeditor/Medi get sourceElement() { return this.#editor.sourceElement; } + get focusTracker() { + return this.#editor.ui.focusTracker; + } } function* findModelForRemoval(element, model, attributes) { if (element.is("element", model)) { From 11f927f3aedb813086a5671a3c404d13f3f097b5 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 08:55:47 +0100 Subject: [PATCH 09/65] Set active editor to past the quote --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 12 +++++++----- .../js/WoltLabSuite/Core/Component/Quote/List.js | 11 ++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index a83d633b2c..0768878da3 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -71,12 +71,14 @@ export function setup(editorId: string): void { listenToCkeditor(editor).ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); - - setActiveEditor(ckeditor, true); - } else { - setActiveEditor(ckeditor, false); } - //TODO handle active editor changed + setActiveEditor(ckeditor, ckeditor.features.quoteBlock); + + ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => { + if (isFocused) { + setActiveEditor(ckeditor, ckeditor.features.quoteBlock); + } + }); }); } 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 179fa611ef..b3ff4a52a5 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -58,12 +58,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C (0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); - (0, Message_1.setActiveEditor)(ckeditor, true); } - else { - (0, Message_1.setActiveEditor)(ckeditor, false); - } - //TODO handle active editor changed + (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); + ckeditor.focusTracker.on("change:isFocused", () => { + if (ckeditor.focusTracker.isFocused) { + (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); + } + }); }); } }); From fb73351072dc762227c16248cc90f5f488689ffd Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 11:13:51 +0100 Subject: [PATCH 10/65] Add new endpoint of render the full quote of a message --- .../Core/Api/Messages/RenderQuote.ts | 29 +++++++ ts/WoltLabSuite/Core/Component/Quote/List.ts | 9 +- .../Core/Component/Quote/Message.ts | 55 +++++++++--- .../Core/Component/Quote/Storage.ts | 85 +++++++++++++++++++ ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 2 +- .../WoltLabSuite/Core/Component/Quote/List.js | 14 +-- .../Core/Component/Quote/Message.js | 39 ++++++--- .../js/WoltLabSuite/Core/Ui/Message/Quote.js | 2 +- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../core/messages/RenderQuote.class.php | 63 ++++++++++++++ 10 files changed, 264 insertions(+), 35 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts create mode 100644 ts/WoltLabSuite/Core/Component/Quote/Storage.ts create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts new file mode 100644 index 0000000000..8f389ff930 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -0,0 +1,29 @@ +/** + * Requests render a full quote of a message. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; + +type Response = string; + +export async function renderQuote(objectType: string, objectID: number): Promise> { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/renderquote"); + url.searchParams.set("objectType", objectType); + url.searchParams.set("objectID", objectID.toString()); + + let response: Response; + try { + response = (await prepareRequest(url).get().fetchAsJson()) as Response; + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue(response); +} diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 0768878da3..b047baacf7 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -8,14 +8,13 @@ * @woltlabExcludeBundle tiny */ -import * as Core from "WoltLabSuite/Core/Core"; import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; +import { getQuotes } from "WoltLabSuite/Core/Component/Quote/Storage"; -export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const quoteLists = new Map(); class QuoteList { @@ -39,7 +38,11 @@ class QuoteList { } public renderQuotes(): void { - this.#container.innerHTML = window.localStorage.getItem(STORAGE_KEY) || ""; + this.#container.innerHTML = ""; + + for (const [, quotes] of getQuotes()) { + // TODO render quotes + } if (this.#container.hasChildNodes()) { getTabMenu(this.#editorId)?.showTab( diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index a8c2665057..b0297ab22b 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -13,27 +13,38 @@ import { getPhrase } from "WoltLabSuite/Core/Language"; import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment"; import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; +import { saveQuote, saveFullQuote } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; interface Container { element: HTMLElement; messageBodySelector: string; + objectType: string; + objectId: number; } +let selectedMessage: + | undefined + | { + message: string; + container: Container; + }; + const containers = new Map(); let activeMessageId = ""; -let message = ""; let activeEditor: CKEditor | undefined = undefined; let timerSelectionChange: number | undefined = undefined; let isMouseDown = false; -let objectId: number | undefined = undefined; const copyQuote = document.createElement("div"); -export function registerContainer(containerSelector: string, messageBodySelector: string): void { +export function registerContainer(containerSelector: string, messageBodySelector: string, objectType: string): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); containers.set(id, { element: container, messageBodySelector: messageBodySelector, + objectType: objectType, + objectId: ~~container.dataset.objectId!, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -43,13 +54,19 @@ export function registerContainer(containerSelector: string, messageBodySelector container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); - container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => { - //TODO - }); + container.querySelector(".jsQuoteMessage")?.addEventListener( + "click", + promiseMutex(async (event: MouseEvent) => { + event.preventDefault(); + + await saveFullQuote(objectType, ~~container.dataset.objectId!); + //TODO insert into `activeEditor` + }), + ); }); } -export function setActiveEditor(editor: CKEditor, supportDirectInsert: boolean) { +export function setActiveEditor(editor?: CKEditor, supportDirectInsert: boolean = false) { copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert")!.hidden = !supportDirectInsert; activeEditor = editor; @@ -63,7 +80,9 @@ function setup() { buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = getPhrase("wcf.message.quote.quoteSelected"); buttonSaveQuote.addEventListener("click", () => { - //TODO + saveQuote(selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.message); + + removeSelection(); }); copyQuote.appendChild(buttonSaveQuote); const buttonSaveAndInsertQuote = document.createElement("button"); @@ -72,7 +91,10 @@ function setup() { buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = getPhrase("wcf.message.quote.quoteAndReply"); buttonSaveAndInsertQuote.addEventListener("click", () => { - //TODO + saveQuote(selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.message); + //TODO insert into `activeEditor` + + removeSelection(); }); copyQuote.appendChild(buttonSaveAndInsertQuote); @@ -386,8 +408,19 @@ function onMouseUp(event?: MouseEvent): void { const text = getSelectedText().trim(); if (text !== "") { copyQuote.classList.add("active"); - message = text; - objectId = ~~container.element.dataset.objectId!; + selectedMessage = { + message: text, + container: container, + }; } }, 10); } + +function removeSelection(): void { + copyQuote.classList.remove("active"); + + const selection = window.getSelection()!; + if (selection.rangeCount) { + selection.removeAllRanges(); + } +} diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts new file mode 100644 index 0000000000..61bd25b830 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -0,0 +1,85 @@ +/** + * Stores the quote data. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ + +import * as Core from "WoltLabSuite/Core/Core"; +import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; + +interface StorageData { + quotes: Map>; +} + +export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; + +export function saveQuote(objectType: string, objectId: number, message: string) { + const storage = getStorage(); + + const key = getKey(objectType, objectId); + if (!storage.quotes.has(key)) { + storage.quotes.set(key, new Set()); + } + + storage.quotes.get(key)!.add(message); + + saveStorage(storage); +} + +export async function saveFullQuote(objectType: string, objectId: number) { + const result = await renderQuote(objectType, objectId); + if (!result.ok) { + // TODO error handling + return; + } + + saveQuote(objectType, objectId, result.value); +} + +export function getQuotes(): Map> { + return getStorage().quotes; +} + +function getStorage(): StorageData { + const data = window.localStorage.getItem(STORAGE_KEY); + if (data === null) { + return { + quotes: new Map(), + }; + } else { + return JSON.parse(data, (key, value) => { + if (key === "quotes") { + const result = new Map>(value); + for (const [key, setValue] of result) { + result.set(key, new Set(setValue)); + } + return result; + } + + return value; + }); + } +} + +function getKey(objectType: string, objectId: number): string { + return `${objectType}:${objectId}`; +} + +function saveStorage(data: StorageData) { + window.localStorage.setItem( + STORAGE_KEY, + JSON.stringify(data, (key, value) => { + if (value instanceof Map) { + return Array.from(value.entries()); + } else if (value instanceof Set) { + return Array.from(value); + } + + return value; + }), + ); +} diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index 8a2ee18458..29b39a52d1 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -25,7 +25,7 @@ export class UiMessageQuote { messageContentSelector: string, supportDirectInsert: boolean, ) { - registerContainer(containerSelector, messageBodySelector); + registerContainer(containerSelector, messageBodySelector, objectType); } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js index b3ff4a52a5..a7a080d342 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -7,14 +7,11 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1, Message_1) { +define(["require", "exports", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage"], function (require, exports, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.STORAGE_KEY = void 0; exports.getQuoteList = getQuoteList; exports.setup = setup; - Core = tslib_1.__importStar(Core); - exports.STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const quoteLists = new Map(); class QuoteList { #container; @@ -33,7 +30,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C this.renderQuotes(); } renderQuotes() { - this.#container.innerHTML = window.localStorage.getItem(exports.STORAGE_KEY) || ""; + this.#container.innerHTML = ""; + for (const [, quotes] of (0, Storage_1.getQuotes)()) { + // TODO render quotes + } if (this.#container.hasChildNodes()) { (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { count: this.#container.childElementCount, @@ -60,8 +60,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); } (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); - ckeditor.focusTracker.on("change:isFocused", () => { - if (ckeditor.focusTracker.isFocused) { + ckeditor.focusTracker.on("change:isFocused", (_evt, _name, isFocused) => { + if (isFocused) { (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); } }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js index 3068f6aad6..e213a4cf2f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -7,38 +7,41 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1, Storage_1, PromiseMutex_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerContainer = registerContainer; exports.setActiveEditor = setActiveEditor; Util_1 = tslib_1.__importDefault(Util_1); + let selectedMessage; const containers = new Map(); let activeMessageId = ""; - let message = ""; let activeEditor = undefined; let timerSelectionChange = undefined; let isMouseDown = false; - let objectId = undefined; const copyQuote = document.createElement("div"); - function registerContainer(containerSelector, messageBodySelector) { + function registerContainer(containerSelector, messageBodySelector, objectType) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); containers.set(id, { element: container, messageBodySelector: messageBodySelector, + objectType: objectType, + objectId: ~~container.dataset.objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { return; } container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); - container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => { - //TODO - }); + container.querySelector(".jsQuoteMessage")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { + event.preventDefault(); + await (0, Storage_1.saveFullQuote)(objectType, ~~container.dataset.objectId); + //TODO insert into `activeEditor` + })); }); } - function setActiveEditor(editor, supportDirectInsert) { + function setActiveEditor(editor, supportDirectInsert = false) { copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert").hidden = !supportDirectInsert; activeEditor = editor; } @@ -49,7 +52,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteSelected"); buttonSaveQuote.addEventListener("click", () => { - //TODO + (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + removeSelection(); }); copyQuote.appendChild(buttonSaveQuote); const buttonSaveAndInsertQuote = document.createElement("button"); @@ -58,7 +62,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteAndReply"); buttonSaveAndInsertQuote.addEventListener("click", () => { - //TODO + (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + //TODO insert into `activeEditor` + removeSelection(); }); copyQuote.appendChild(buttonSaveAndInsertQuote); document.body.appendChild(copyQuote); @@ -303,9 +309,18 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui const text = getSelectedText().trim(); if (text !== "") { copyQuote.classList.add("active"); - message = text; - objectId = ~~container.element.dataset.objectId; + selectedMessage = { + message: text, + container: container, + }; } }, 10); } + function removeSelection() { + copyQuote.classList.remove("active"); + const selection = window.getSelection(); + if (selection.rangeCount) { + selection.removeAllRanges(); + } + } }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js index fa5784e730..9399bb00c2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -12,7 +12,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func * Initializes the quote handler for given object type. */ constructor(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) { - (0, Message_1.registerContainer)(containerSelector, messageBodySelector); + (0, Message_1.registerContainer)(containerSelector, messageBodySelector, objectType); } } exports.UiMessageQuote = UiMessageQuote; diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 5c673485c3..c937638498 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -137,6 +137,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\comments\responses\UpdateResponse()); $event->register(new \wcf\system\endpoint\controller\core\cronjobs\logs\ClearLogs()); $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions()); + $event->register(new \wcf\system\endpoint\controller\core\messages\RenderQuote()); $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession()); $event->register(new \wcf\system\endpoint\controller\core\versionTrackers\RevertVersion()); $event->register(new \wcf\system\endpoint\controller\core\moderationQueues\ChangeJustifiedStatus()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php new file mode 100644 index 0000000000..76d6ba53cd --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php @@ -0,0 +1,63 @@ + + * @since 6.2 + */ +#[GetRequest('/core/messages/renderquote')] +final class RenderQuote implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $parameters = Helper::mapApiParameters($request, GetRenderQuoteParameters::class); + + return new JsonResponse( + $this->renderFullQuote($parameters), + 200, + ); + } + + private function renderFullQuote(GetRenderQuoteParameters $parameters): string + { + // TODO load object + /** @var $object IMessage */ + // TODO load embedded objects? + + $htmlInputProcessor = new HtmlInputProcessor(); + $htmlInputProcessor->processIntermediate($object->getMessage()); + + if (MESSAGE_MAX_QUOTE_DEPTH) { + $htmlInputProcessor->enforceQuoteDepth(MESSAGE_MAX_QUOTE_DEPTH - 1, true); + } + + return $htmlInputProcessor->getHtml(); + } +} + +/** @internal */ +final class GetRenderQuoteParameters +{ + public function __construct( + /** @var non-empty-string */ + public readonly string $objectType, + /** @var positive-int */ + public readonly int $objectID, + ) { + } +} From b828e9b6d83007e7243cae4c0da869f663fe4db6 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 11:56:38 +0100 Subject: [PATCH 11/65] Store information about the message author --- ts/WoltLabSuite/Core/Api/Messages/Author.ts | 36 +++++++++++ .../Core/Api/Messages/RenderQuote.ts | 18 +++++- .../Core/Component/Quote/Message.ts | 49 ++++++++++---- .../Core/Component/Quote/Storage.ts | 63 ++++++++++++++---- ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 7 +- .../Core/Component/Quote/Message.js | 17 ++--- .../js/WoltLabSuite/Core/Ui/Message/Quote.js | 6 +- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../core/messages/GetMessageAuthor.class.php | 64 +++++++++++++++++++ .../core/messages/RenderQuote.class.php | 24 +++++-- 10 files changed, 242 insertions(+), 43 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Api/Messages/Author.ts create 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 new file mode 100644 index 0000000000..8a850bc06f --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Messages/Author.ts @@ -0,0 +1,36 @@ +/** + * 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; + authorID: number; + author: string; + time: number; + link: string; + avatar: string; +}; + +export async function messageAuthor(className: string, objectID: number): Promise> { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/messageauthor"); + 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 8f389ff930..11fda558e1 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -11,11 +11,25 @@ import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; -type Response = string; +type Response = { + objectID: number; + authorID: number; + author: string; + time: number; + link: string; + avatar: string; + message: string; +}; -export async function renderQuote(objectType: string, objectID: number): Promise> { +export async function renderQuote( + objectType: string, + className: string, + objectID: number, +): Promise> { const url = new URL(window.WSC_RPC_API_URL + "core/messages/renderquote"); url.searchParams.set("objectType", objectType); + url.searchParams.set("className", className); + url.searchParams.set("fullQuote", "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 b0297ab22b..92fb91042c 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -20,6 +20,7 @@ interface Container { element: HTMLElement; messageBodySelector: string; objectType: string; + className: string; objectId: number; } @@ -37,13 +38,19 @@ let timerSelectionChange: number | undefined = undefined; let isMouseDown = false; const copyQuote = document.createElement("div"); -export function registerContainer(containerSelector: string, messageBodySelector: string, objectType: string): void { +export function registerContainer( + containerSelector: string, + messageBodySelector: string, + className: string, + objectType: string, +): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); containers.set(id, { element: container, messageBodySelector: messageBodySelector, objectType: objectType, + className: className, objectId: ~~container.dataset.objectId!, }); @@ -59,7 +66,7 @@ export function registerContainer(containerSelector: string, messageBodySelector promiseMutex(async (event: MouseEvent) => { event.preventDefault(); - await saveFullQuote(objectType, ~~container.dataset.objectId!); + await saveFullQuote(objectType, className, ~~container.dataset.objectId!); //TODO insert into `activeEditor` }), ); @@ -79,23 +86,39 @@ function setup() { buttonSaveQuote.type = "button"; buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = getPhrase("wcf.message.quote.quoteSelected"); - buttonSaveQuote.addEventListener("click", () => { - saveQuote(selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.message); - - removeSelection(); - }); + buttonSaveQuote.addEventListener( + "click", + promiseMutex(async () => { + await saveQuote( + selectedMessage!.container.objectType, + selectedMessage!.container.objectId, + selectedMessage!.container.className, + selectedMessage!.message, + ); + + removeSelection(); + }), + ); copyQuote.appendChild(buttonSaveQuote); const buttonSaveAndInsertQuote = document.createElement("button"); buttonSaveAndInsertQuote.type = "button"; buttonSaveAndInsertQuote.hidden = true; buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = getPhrase("wcf.message.quote.quoteAndReply"); - buttonSaveAndInsertQuote.addEventListener("click", () => { - saveQuote(selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.message); - //TODO insert into `activeEditor` - - removeSelection(); - }); + buttonSaveAndInsertQuote.addEventListener( + "click", + promiseMutex(async () => { + await saveQuote( + selectedMessage!.container.objectType, + selectedMessage!.container.objectId, + selectedMessage!.container.className, + selectedMessage!.message, + ); + //TODO insert into `activeEditor` + + removeSelection(); + }), + ); copyQuote.appendChild(buttonSaveAndInsertQuote); document.body.appendChild(copyQuote); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 61bd25b830..78b74ba854 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -10,34 +10,68 @@ import * as Core from "WoltLabSuite/Core/Core"; import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; +import { messageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; + +interface Message { + objectID: number; + time: number; + link: string; + authorID: number; + author: string; + avatar: string; +} interface StorageData { quotes: Map>; + messages: Map; } -export const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; - -export function saveQuote(objectType: string, objectId: number, message: string) { - const storage = getStorage(); +const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; - const key = getKey(objectType, objectId); - if (!storage.quotes.has(key)) { - storage.quotes.set(key, new Set()); +export async function saveQuote(objectType: string, objectId: number, objectClassName: string, message: string) { + const result = await messageAuthor(objectClassName, objectId); + if (!result.ok) { + // TODO error handling + return; } - storage.quotes.get(key)!.add(message); - - saveStorage(storage); + storeQuote(objectType, result.value, message); } -export async function saveFullQuote(objectType: string, objectId: number) { - const result = await renderQuote(objectType, objectId); +export async function saveFullQuote(objectType: string, objectClassName: string, objectId: number) { + const result = await renderQuote(objectType, objectClassName, objectId); if (!result.ok) { // TODO error handling return; } - saveQuote(objectType, objectId, result.value); + storeQuote( + objectType, + { + objectID: result.value.objectID, + time: result.value.time, + link: result.value.link, + authorID: result.value.authorID, + author: result.value.author, + avatar: result.value.avatar, + }, + result.value.message, + ); +} + +function storeQuote(objectType: string, message: Message, quote: string): void { + const storage = getStorage(); + + const key = getKey(objectType, message.objectID); + if (!storage.quotes.has(key)) { + storage.quotes.set(key, new Set()); + } + + storage.messages.set(key, message); + + storage.quotes.get(key)!.add(quote); + + saveStorage(storage); } export function getQuotes(): Map> { @@ -49,6 +83,7 @@ function getStorage(): StorageData { if (data === null) { return { quotes: new Map(), + messages: new Map(), }; } else { return JSON.parse(data, (key, value) => { @@ -58,6 +93,8 @@ function getStorage(): StorageData { result.set(key, new Set(setValue)); } return result; + } else if (key === "messages") { + return new Map(value); } return value; diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index 29b39a52d1..491e974740 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -25,7 +25,12 @@ export class UiMessageQuote { messageContentSelector: string, supportDirectInsert: boolean, ) { - registerContainer(containerSelector, messageBodySelector, objectType); + // remove "Action" from className + 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 e213a4cf2f..72f49b9485 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -20,13 +20,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui let timerSelectionChange = undefined; let isMouseDown = false; const copyQuote = document.createElement("div"); - function registerContainer(containerSelector, messageBodySelector, objectType) { + function registerContainer(containerSelector, messageBodySelector, className, objectType) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); containers.set(id, { element: container, messageBodySelector: messageBodySelector, objectType: objectType, + className: className, objectId: ~~container.dataset.objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -36,7 +37,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui container.classList.add("jsQuoteMessageContainer"); container.querySelector(".jsQuoteMessage")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { event.preventDefault(); - await (0, Storage_1.saveFullQuote)(objectType, ~~container.dataset.objectId); + await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); //TODO insert into `activeEditor` })); }); @@ -51,21 +52,21 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveQuote.type = "button"; buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteSelected"); - buttonSaveQuote.addEventListener("click", () => { - (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + buttonSaveQuote.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { + await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); removeSelection(); - }); + })); copyQuote.appendChild(buttonSaveQuote); const buttonSaveAndInsertQuote = document.createElement("button"); buttonSaveAndInsertQuote.type = "button"; buttonSaveAndInsertQuote.hidden = true; buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteAndReply"); - buttonSaveAndInsertQuote.addEventListener("click", () => { - (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message); + buttonSaveAndInsertQuote.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { + await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); //TODO insert into `activeEditor` removeSelection(); - }); + })); copyQuote.appendChild(buttonSaveAndInsertQuote); document.body.appendChild(copyQuote); document.addEventListener("mouseup", (event) => onMouseUp(event)); 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 9399bb00c2..9829985755 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -12,7 +12,11 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func * Initializes the quote handler for given object type. */ constructor(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) { - (0, Message_1.registerContainer)(containerSelector, messageBodySelector, objectType); + // remove "Action" from className + if (className.endsWith("Action")) { + className = className.substring(0, className.length - 6); + } + (0, Message_1.registerContainer)(containerSelector, messageBodySelector, className, objectType); } } exports.UiMessageQuote = UiMessageQuote; diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index c937638498..8110ba1e3e 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -138,6 +138,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\cronjobs\logs\ClearLogs()); $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions()); $event->register(new \wcf\system\endpoint\controller\core\messages\RenderQuote()); + $event->register(new \wcf\system\endpoint\controller\core\messages\GetMessageAuthor()); $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession()); $event->register(new \wcf\system\endpoint\controller\core\versionTrackers\RevertVersion()); $event->register(new \wcf\system\endpoint\controller\core\moderationQueues\ChangeJustifiedStatus()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php new file mode 100644 index 0000000000..e378a79dd8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php @@ -0,0 +1,64 @@ + + * @since 6.2 + */ +#[GetRequest('/core/messages/messageauthor')] +final class GetMessageAuthor implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $parameters = Helper::mapApiParameters($request, GetMessageAuthorParameters::class); + + $object = Helper::fetchObjectFromRequestParameter($parameters->objectID, $parameters->className); + \assert($object instanceof IMessage); + + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); + + return new JsonResponse( + [ + "objectID" => $object->getObjectID(), + "authorID" => $userProfile->getUserID(), + "author" => $userProfile->getUsername(), + "avatar" => $userProfile->getAvatar()->getURL(), + "time" => $object->getTime(), + "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, + ) { + } +} 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 76d6ba53cd..e6868a7fbf 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 @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use wcf\data\IMessage; use wcf\http\Helper; +use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\endpoint\GetRequest; use wcf\system\endpoint\IController; use wcf\system\html\input\HtmlInputProcessor; @@ -19,6 +20,7 @@ * @license GNU Lesser General Public License * @since 6.2 */ + #[GetRequest('/core/messages/renderquote')] final class RenderQuote implements IController { @@ -27,16 +29,27 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetRenderQuoteParameters::class); + $object = Helper::fetchObjectFromRequestParameter($parameters->objectID, $parameters->className); + \assert($object instanceof IMessage); + + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); + return new JsonResponse( - $this->renderFullQuote($parameters), + [ + "objectID" => $object->getObjectID(), + "authorID" => $userProfile->getUserID(), + "author" => $userProfile->getUsername(), + "avatar" => $userProfile->getAvatar()->getURL(), + "time" => $object->getTime(), + "link" => $object->getLink(), + "message" => $parameters->fullQuote ? $this->renderFullQuote($object) : "" + ], 200, ); } - private function renderFullQuote(GetRenderQuoteParameters $parameters): string + private function renderFullQuote(IMessage $object): string { - // TODO load object - /** @var $object IMessage */ // TODO load embedded objects? $htmlInputProcessor = new HtmlInputProcessor(); @@ -55,9 +68,10 @@ final class GetRenderQuoteParameters { public function __construct( /** @var non-empty-string */ - public readonly string $objectType, + public readonly string $className, /** @var positive-int */ public readonly int $objectID, + public readonly bool $fullQuote = false, ) { } } From 629f005ceff4f627073864488595c2c28893c770 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 12:39:28 +0100 Subject: [PATCH 12/65] Add basic template for quote list --- ts/WoltLabSuite/Core/Api/Messages/Author.ts | 3 +- .../Core/Api/Messages/RenderQuote.ts | 3 +- ts/WoltLabSuite/Core/Component/Quote/List.ts | 52 ++++++++++++++++++- .../Core/Component/Quote/Storage.ts | 10 +++- .../WoltLabSuite/Core/Component/Quote/List.js | 49 ++++++++++++++++- .../core/messages/GetMessageAuthor.class.php | 3 +- .../core/messages/RenderQuote.class.php | 3 +- 7 files changed, 114 insertions(+), 9 deletions(-) diff --git a/ts/WoltLabSuite/Core/Api/Messages/Author.ts b/ts/WoltLabSuite/Core/Api/Messages/Author.ts index 8a850bc06f..2285cd63b6 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/Author.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/Author.ts @@ -15,7 +15,8 @@ type Response = { objectID: number; authorID: number; author: string; - time: number; + time: string; + title: string; link: string; avatar: string; }; diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index 11fda558e1..85195f9474 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -15,8 +15,9 @@ type Response = { objectID: number; authorID: number; author: string; - time: number; + time: string; link: string; + title: string; avatar: string; message: string; }; diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index b047baacf7..83ff19aa22 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -13,7 +13,8 @@ import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; -import { getQuotes } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { getQuotes, getMessage } from "WoltLabSuite/Core/Component/Quote/Storage"; +import DomUtil from "WoltLabSuite/Core/Dom/Util"; const quoteLists = new Map(); @@ -40,7 +41,54 @@ class QuoteList { public renderQuotes(): void { this.#container.innerHTML = ""; - for (const [, quotes] of getQuotes()) { + for (const [key, quotes] of getQuotes()) { + const message = getMessage(key)!; + + // TODO escape values + // TODO create web components??? + this.#container.append( + DomUtil.createFragmentFromHtml(`
    +
    +
    +
    + + +
    +

    + ${message.title} +

    + +
    +
    +
    +
    +
    +
      + ${Array.from(quotes) + .map( + (quote) => `
    • + + + + + +
      + ${quote} +
      +
    • `, + ) + .join("")} +
    +
    +
    +
    +
    `), + ); // TODO render quotes } diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 78b74ba854..788894b304 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -14,7 +14,8 @@ import { messageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; interface Message { objectID: number; - time: number; + time: string; + title: string; link: string; authorID: number; author: string; @@ -50,6 +51,7 @@ export async function saveFullQuote(objectType: string, objectClassName: string, { objectID: result.value.objectID, time: result.value.time, + title: result.value.title, link: result.value.link, authorID: result.value.authorID, author: result.value.author, @@ -78,6 +80,12 @@ export function getQuotes(): Map> { return getStorage().quotes; } +export function getMessage(objectType: string, objectId?: number): Message | undefined { + const key = objectId ? getKey(objectType, objectId) : objectType; + + return getStorage().messages.get(key); +} + function getStorage(): StorageData { const data = window.localStorage.getItem(STORAGE_KEY); if (data === null) { 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 a7a080d342..9cb14f1db3 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -7,11 +7,12 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage"], function (require, exports, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1, Util_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getQuoteList = getQuoteList; exports.setup = setup; + Util_1 = tslib_1.__importDefault(Util_1); const quoteLists = new Map(); class QuoteList { #container; @@ -31,7 +32,51 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Ckeditor/Event", "Wol } renderQuotes() { this.#container.innerHTML = ""; - for (const [, quotes] of (0, Storage_1.getQuotes)()) { + for (const [key, quotes] of (0, Storage_1.getQuotes)()) { + const message = (0, Storage_1.getMessage)(key); + // TODO escape values + // TODO create web components??? + this.#container.append(Util_1.default.createFragmentFromHtml(`
    +
    +
    +
    + + +
    +

    + ${message.title} +

    + +
    +
    +
    +
    +
    +
      + ${Array.from(quotes) + .map((quote) => `
    • + + + + + +
      + +
      +
    • `) + .join("")} +
    +
    +
    +
    +
    `)); // TODO render quotes } if (this.#container.hasChildNodes()) { 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 e378a79dd8..a6022686fc 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,8 +37,9 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res "objectID" => $object->getObjectID(), "authorID" => $userProfile->getUserID(), "author" => $userProfile->getUsername(), + "title" => $object->getTitle(), "avatar" => $userProfile->getAvatar()->getURL(), - "time" => $object->getTime(), + "time" => (new \DateTime('@' . $object->getTime()))->format("c"), "link" => $object->getLink(), ], 200, 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 e6868a7fbf..21c1103cb2 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 @@ -40,7 +40,8 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res "authorID" => $userProfile->getUserID(), "author" => $userProfile->getUsername(), "avatar" => $userProfile->getAvatar()->getURL(), - "time" => $object->getTime(), + "time" => (new \DateTime('@' . $object->getTime()))->format("c"), + "title" => $object->getTitle(), "link" => $object->getLink(), "message" => $parameters->fullQuote ? $this->renderFullQuote($object) : "" ], From 8dc0b62cea234d068258f6842ae81e31f19d710c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 13:09:04 +0100 Subject: [PATCH 13/65] Refresh quote list, if new quote added --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 6 ++ .../Core/Component/Quote/Storage.ts | 3 + .../WoltLabSuite/Core/Api/Messages/Author.js | 27 +++++ .../Core/Api/Messages/RenderQuote.js | 29 ++++++ .../WoltLabSuite/Core/Component/Quote/List.js | 8 +- .../Core/Component/Quote/Storage.js | 99 +++++++++++++++++++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 83ff19aa22..9445fa9578 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -109,6 +109,12 @@ export function getQuoteList(editorId: string): QuoteList | undefined { return quoteLists.get(editorId); } +export function refreshQuoteLists() { + for (const quoteList of quoteLists.values()) { + quoteList.renderQuotes(); + } +} + export function setup(editorId: string): void { if (quoteLists.has(editorId)) { return; diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 788894b304..8c75371ddb 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -11,6 +11,7 @@ import * as Core from "WoltLabSuite/Core/Core"; import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; import { messageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; +import { refreshQuoteLists } from "WoltLabSuite/Core/Component/Quote/List"; interface Message { objectID: number; @@ -37,6 +38,8 @@ export async function saveQuote(objectType: string, objectId: number, objectClas } storeQuote(objectType, result.value, message); + + refreshQuoteLists(); } export async function saveFullQuote(objectType: string, objectClassName: string, objectId: number) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js new file mode 100644 index 0000000000..bb3051cc90 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js @@ -0,0 +1,27 @@ +/** + * 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.messageAuthor = messageAuthor; + async function messageAuthor(className, objectID) { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/messageauthor"); + 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 new file mode 100644 index 0000000000..aed4fcbe42 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js @@ -0,0 +1,29 @@ +/** + * Requests render a full quote of a message. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * @woltlabExcludeBundle tiny + */ +define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.renderQuote = renderQuote; + async function renderQuote(objectType, className, objectID) { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/renderquote"); + url.searchParams.set("objectType", objectType); + url.searchParams.set("className", className); + url.searchParams.set("fullQuote", "true"); + url.searchParams.set("objectID", objectID.toString()); + let response; + try { + response = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson()); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)(response); + } +}); 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 9cb14f1db3..a8e1c59bf3 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -11,6 +11,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getQuoteList = getQuoteList; + exports.refreshQuoteLists = refreshQuoteLists; exports.setup = setup; Util_1 = tslib_1.__importDefault(Util_1); const quoteLists = new Map(); @@ -66,9 +67,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
    -
    `) .join("")} @@ -92,6 +91,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve function getQuoteList(editorId) { return quoteLists.get(editorId); } + function refreshQuoteLists() { + for (const quoteList of quoteLists.values()) { + quoteList.renderQuotes(); + } + } function setup(editorId) { if (quoteLists.has(editorId)) { return; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js new file mode 100644 index 0000000000..4de88f6177 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -0,0 +1,99 @@ +/** + * Stores the quote data. + * + * @author Olaf Braun + * @copyright 2001-2024 WoltLab GmbH + * @license GNU Lesser General Public License + * @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"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.saveQuote = saveQuote; + exports.saveFullQuote = saveFullQuote; + exports.getQuotes = getQuotes; + exports.getMessage = getMessage; + Core = tslib_1.__importStar(Core); + const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; + async function saveQuote(objectType, objectId, objectClassName, message) { + const result = await (0, Author_1.messageAuthor)(objectClassName, objectId); + if (!result.ok) { + // TODO error handling + return; + } + storeQuote(objectType, result.value, message); + (0, List_1.refreshQuoteLists)(); + } + async function saveFullQuote(objectType, objectClassName, objectId) { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectClassName, objectId); + if (!result.ok) { + // TODO error handling + return; + } + storeQuote(objectType, { + 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, + }, result.value.message); + } + function storeQuote(objectType, message, quote) { + const storage = getStorage(); + const key = getKey(objectType, message.objectID); + if (!storage.quotes.has(key)) { + storage.quotes.set(key, new Set()); + } + storage.messages.set(key, message); + storage.quotes.get(key).add(quote); + saveStorage(storage); + } + function getQuotes() { + return getStorage().quotes; + } + function getMessage(objectType, objectId) { + const key = objectId ? getKey(objectType, objectId) : objectType; + return getStorage().messages.get(key); + } + function getStorage() { + const data = window.localStorage.getItem(STORAGE_KEY); + if (data === null) { + return { + quotes: new Map(), + messages: new Map(), + }; + } + else { + return JSON.parse(data, (key, value) => { + if (key === "quotes") { + const result = new Map(value); + for (const [key, setValue] of result) { + result.set(key, new Set(setValue)); + } + return result; + } + else if (key === "messages") { + return new Map(value); + } + return value; + }); + } + } + function getKey(objectType, objectId) { + return `${objectType}:${objectId}`; + } + function saveStorage(data) { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data, (key, value) => { + if (value instanceof Map) { + return Array.from(value.entries()); + } + else if (value instanceof Set) { + return Array.from(value); + } + return value; + })); + } +}); From 126bcda9a8e115e1ddd5bd6096247bcfab859d43 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 19 Dec 2024 13:11:05 +0100 Subject: [PATCH 14/65] Add function to remove quotes --- .../Core/Component/Quote/Storage.ts | 40 ++++++++++++++----- .../Core/Component/Quote/Storage.js | 29 ++++++++++---- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 8c75371ddb..a4bdfbb545 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -64,6 +64,36 @@ export async function saveFullQuote(objectType: string, objectClassName: string, ); } +export function getQuotes(): Map> { + return getStorage().quotes; +} + +export function getMessage(objectType: string, objectId?: number): Message | undefined { + const key = objectId ? getKey(objectType, objectId) : objectType; + + return getStorage().messages.get(key); +} + +export function removeQuote(objectType: string, objectId: number, quote: string): void { + const storage = getStorage(); + + const key = getKey(objectType, objectId); + if (!storage.quotes.has(key)) { + return; + } + + storage.quotes.get(key)!.delete(quote); + + if (storage.quotes.get(key)!.size === 0) { + storage.quotes.delete(key); + storage.messages.delete(key); + } + + saveStorage(storage); + + refreshQuoteLists(); +} + function storeQuote(objectType: string, message: Message, quote: string): void { const storage = getStorage(); @@ -79,16 +109,6 @@ function storeQuote(objectType: string, message: Message, quote: string): void { saveStorage(storage); } -export function getQuotes(): Map> { - return getStorage().quotes; -} - -export function getMessage(objectType: string, objectId?: number): Message | undefined { - const key = objectId ? getKey(objectType, objectId) : objectType; - - return getStorage().messages.get(key); -} - function getStorage(): StorageData { const data = window.localStorage.getItem(STORAGE_KEY); if (data === null) { 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 4de88f6177..fd6e14848d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -14,6 +14,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C exports.saveFullQuote = saveFullQuote; exports.getQuotes = getQuotes; exports.getMessage = getMessage; + exports.removeQuote = removeQuote; Core = tslib_1.__importStar(Core); const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; async function saveQuote(objectType, objectId, objectClassName, message) { @@ -41,6 +42,27 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C avatar: result.value.avatar, }, result.value.message); } + function getQuotes() { + return getStorage().quotes; + } + function getMessage(objectType, objectId) { + const key = objectId ? getKey(objectType, objectId) : objectType; + return getStorage().messages.get(key); + } + function removeQuote(objectType, objectId, quote) { + const storage = getStorage(); + const key = getKey(objectType, objectId); + if (!storage.quotes.has(key)) { + return; + } + storage.quotes.get(key).delete(quote); + if (storage.quotes.get(key).size === 0) { + storage.quotes.delete(key); + storage.messages.delete(key); + } + saveStorage(storage); + (0, List_1.refreshQuoteLists)(); + } function storeQuote(objectType, message, quote) { const storage = getStorage(); const key = getKey(objectType, message.objectID); @@ -51,13 +73,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C storage.quotes.get(key).add(quote); saveStorage(storage); } - function getQuotes() { - return getStorage().quotes; - } - function getMessage(objectType, objectId) { - const key = objectId ? getKey(objectType, objectId) : objectType; - return getStorage().messages.get(key); - } function getStorage() { const data = window.localStorage.getItem(STORAGE_KEY); if (data === null) { From f247a1a66d53e3f94357b7b54e39525845d7c86e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 20 Dec 2024 14:41:10 +0100 Subject: [PATCH 15/65] Add `IEmbeddedMessageObject` to load embedded object --- .../Core/Api/Messages/RenderQuote.ts | 3 +- ts/WoltLabSuite/Core/Component/Quote/List.ts | 40 +++++++++++++------ .../Core/Component/Quote/Storage.ts | 32 +++++++++++---- .../WoltLabSuite/Core/Component/Quote/List.js | 29 ++++++++++---- .../Core/Component/Quote/Storage.js | 16 ++++++-- .../lib/data/IEmbeddedMessageObject.class.php | 19 +++++++++ .../core/messages/RenderQuote.class.php | 10 +++-- ...PreloadPhrasesCollectingListener.class.php | 1 + 8 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index 85195f9474..fde2cdc377 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -19,7 +19,8 @@ type Response = { link: string; title: string; avatar: string; - message: string; + message: string | null; + rawMessage: string | null; }; export async function renderQuote( diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 9445fa9578..8fe2d35578 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -8,8 +8,7 @@ * @woltlabExcludeBundle tiny */ -import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; -import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; +import { listenToCkeditor, dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; @@ -20,10 +19,10 @@ const quoteLists = new Map(); class QuoteList { #container: HTMLElement; - #editor: CKEditor; + #editor: HTMLElement; #editorId: string; - constructor(editorId: string, editor: CKEditor) { + constructor(editorId: string, editor: HTMLElement) { this.#editorId = editorId; this.#editor = editor; this.#container = document.getElementById(`quotes_${editorId}`)!; @@ -41,13 +40,14 @@ class QuoteList { public renderQuotes(): void { this.#container.innerHTML = ""; + let quotesCount = 0; for (const [key, quotes] of getQuotes()) { const message = getMessage(key)!; + quotesCount += quotes.size; // TODO escape values // TODO create web components??? - this.#container.append( - DomUtil.createFragmentFromHtml(`
    + const fragment = DomUtil.createFragmentFromHtml(`
    @@ -74,11 +74,12 @@ class QuoteList {
    - ${quote} + ${quote.message}
    `, ) @@ -87,16 +88,29 @@ class QuoteList {
    -`), - ); - // TODO render quotes +`); + + fragment.querySelectorAll(".jsInsertQuote").forEach((button) => { + button.addEventListener("click", () => { + // TODO dont query the DOM + // TODO use rawMessage to insert if available otherwise use message + dispatchToCkeditor(this.#editor).insertQuote({ + author: message.author, + content: button.closest("li")!.querySelector(".jsQuote")!.innerHTML, + isText: false, + link: message.link, + }); + }); + }); + + this.#container.append(fragment); } - if (this.#container.hasChildNodes()) { + if (quotesCount > 0) { getTabMenu(this.#editorId)?.showTab( "quotes", getPhrase("wcf.message.quote.showQuotes", { - count: this.#container.childElementCount, + count: quotesCount, }), ); } else { @@ -127,7 +141,7 @@ export function setup(editorId: string): void { listenToCkeditor(editor).ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { - quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); + quoteLists.set(editorId, new QuoteList(editorId, editor)); } setActiveEditor(ckeditor, ckeditor.features.quoteBlock); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index a4bdfbb545..314173fd44 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -23,8 +23,13 @@ interface Message { avatar: string; } +interface Quote { + message: string; + rawMessage?: string; +} + interface StorageData { - quotes: Map>; + quotes: Map>; messages: Map; } @@ -37,7 +42,9 @@ export async function saveQuote(objectType: string, objectId: number, objectClas return; } - storeQuote(objectType, result.value, message); + storeQuote(objectType, result.value, { + message, + }); refreshQuoteLists(); } @@ -60,11 +67,16 @@ export async function saveFullQuote(objectType: string, objectClassName: string, author: result.value.author, avatar: result.value.avatar, }, - result.value.message, + { + message: result.value.message!, + rawMessage: result.value.rawMessage!, + }, ); + + refreshQuoteLists(); } -export function getQuotes(): Map> { +export function getQuotes(): Map> { return getStorage().quotes; } @@ -74,7 +86,7 @@ export function getMessage(objectType: string, objectId?: number): Message | und return getStorage().messages.get(key); } -export function removeQuote(objectType: string, objectId: number, quote: string): void { +export function removeQuote(objectType: string, objectId: number, quote: Quote): void { const storage = getStorage(); const key = getKey(objectType, objectId); @@ -94,7 +106,7 @@ export function removeQuote(objectType: string, objectId: number, quote: string) refreshQuoteLists(); } -function storeQuote(objectType: string, message: Message, quote: string): void { +function storeQuote(objectType: string, message: Message, quote: Quote): void { const storage = getStorage(); const key = getKey(objectType, message.objectID); @@ -104,7 +116,13 @@ function storeQuote(objectType: string, message: Message, quote: string): void { storage.messages.set(key, message); - storage.quotes.get(key)!.add(quote); + if ( + !Array.from(storage.quotes.get(key)!) + .map((q) => q.message) + .includes(quote.message) + ) { + storage.quotes.get(key)!.add(quote); + } saveStorage(storage); } 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 a8e1c59bf3..d2dd115fc7 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -33,11 +33,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve } renderQuotes() { this.#container.innerHTML = ""; + let quotesCount = 0; for (const [key, quotes] of (0, Storage_1.getQuotes)()) { const message = (0, Storage_1.getMessage)(key); + quotesCount += quotes.size; // TODO escape values // TODO create web components??? - this.#container.append(Util_1.default.createFragmentFromHtml(`
    + const fragment = Util_1.default.createFragmentFromHtml(`
    @@ -63,11 +65,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
    - ${quote} + ${quote.message}
    `) .join("")} @@ -75,12 +78,24 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
    -`)); - // TODO render quotes +`); + fragment.querySelectorAll(".jsInsertQuote").forEach((button) => { + button.addEventListener("click", () => { + // TODO dont query the DOM + // TODO use rawMessage to insert if available otherwise use message + (0, Event_1.dispatchToCkeditor)(this.#editor).insertQuote({ + author: message.author, + content: button.closest("li").querySelector(".jsQuote").innerHTML, + isText: false, + link: message.link, + }); + }); + }); + this.#container.append(fragment); } - if (this.#container.hasChildNodes()) { + if (quotesCount > 0) { (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { - count: this.#container.childElementCount, + count: quotesCount, })); } else { @@ -106,7 +121,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve } (0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { - quoteLists.set(editorId, new QuoteList(editorId, ckeditor)); + quoteLists.set(editorId, new QuoteList(editorId, editor)); } (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); ckeditor.focusTracker.on("change:isFocused", (_evt, _name, isFocused) => { 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 fd6e14848d..b71b4f69be 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -23,7 +23,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C // TODO error handling return; } - storeQuote(objectType, result.value, message); + storeQuote(objectType, result.value, { + message, + }); (0, List_1.refreshQuoteLists)(); } async function saveFullQuote(objectType, objectClassName, objectId) { @@ -40,7 +42,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C authorID: result.value.authorID, author: result.value.author, avatar: result.value.avatar, - }, result.value.message); + }, { + message: result.value.message, + rawMessage: result.value.rawMessage, + }); + (0, List_1.refreshQuoteLists)(); } function getQuotes() { return getStorage().quotes; @@ -70,7 +76,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C storage.quotes.set(key, new Set()); } storage.messages.set(key, message); - storage.quotes.get(key).add(quote); + if (!Array.from(storage.quotes.get(key)) + .map((q) => q.message) + .includes(quote.message)) { + storage.quotes.get(key).add(quote); + } saveStorage(storage); } function getStorage() { diff --git a/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php b/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php new file mode 100644 index 0000000000..da483e1eb6 --- /dev/null +++ b/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php @@ -0,0 +1,19 @@ + + * @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/system/endpoint/controller/core/messages/RenderQuote.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php index 21c1103cb2..aec19a6a4d 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,6 +5,7 @@ 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\http\Helper; use wcf\system\cache\runtime\UserProfileRuntimeCache; @@ -34,6 +35,10 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); + if ($object instanceof IEmbeddedMessageObject) { + $object->loadEmbeddedObjects(); + } + return new JsonResponse( [ "objectID" => $object->getObjectID(), @@ -43,7 +48,8 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res "time" => (new \DateTime('@' . $object->getTime()))->format("c"), "title" => $object->getTitle(), "link" => $object->getLink(), - "message" => $parameters->fullQuote ? $this->renderFullQuote($object) : "" + "rawMessage" => $parameters->fullQuote ? $this->renderFullQuote($object) : null, + "message" => $parameters->fullQuote ? $object->getFormattedMessage() : null ], 200, ); @@ -51,8 +57,6 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res private function renderFullQuote(IMessage $object): string { - // TODO load embedded objects? - $htmlInputProcessor = new HtmlInputProcessor(); $htmlInputProcessor->processIntermediate($object->getMessage()); diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index 8aaf4fe612..8cbac407ea 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -143,6 +143,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.message.quote.quoteAndReply'); $event->preload('wcf.message.quote.removeAllQuotes'); $event->preload('wcf.message.quote.showQuotes'); + $event->preload('wcf.message.quote.insertQuote'); $event->preload('wcf.moderation.report.reportContent'); From a1f49087a618d138e6e8586d3b5d3d705db06e5d Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 20 Dec 2024 14:46:29 +0100 Subject: [PATCH 16/65] Handle guest objects --- ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts | 2 +- ts/WoltLabSuite/Core/Component/Quote/List.ts | 2 +- ts/WoltLabSuite/Core/Component/Quote/Storage.ts | 2 +- .../endpoint/controller/core/messages/RenderQuote.class.php | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index fde2cdc377..179633d3b2 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -13,7 +13,7 @@ import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; type Response = { objectID: number; - authorID: number; + authorID: number | null; author: string; time: string; link: string; diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 8fe2d35578..680f905f7f 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -90,9 +90,9 @@ class QuoteList { `); + // TODO dont query the DOM fragment.querySelectorAll(".jsInsertQuote").forEach((button) => { button.addEventListener("click", () => { - // TODO dont query the DOM // TODO use rawMessage to insert if available otherwise use message dispatchToCkeditor(this.#editor).insertQuote({ author: message.author, diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 314173fd44..5a9413a114 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -18,7 +18,7 @@ interface Message { time: string; title: string; link: string; - authorID: number; + authorID: number | null; author: string; avatar: string; } 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 aec19a6a4d..c52807261a 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 @@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface; use wcf\data\IEmbeddedMessageObject; use wcf\data\IMessage; +use wcf\data\user\UserProfile; use wcf\http\Helper; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\endpoint\GetRequest; @@ -34,6 +35,9 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res \assert($object instanceof IMessage); $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); + if ($userProfile === null) { + $userProfile = UserProfile::getGuestUserProfile($object->getUsername()); + } if ($object instanceof IEmbeddedMessageObject) { $object->loadEmbeddedObjects(); From aa6a8a742b630848a06fe9ac92970cf98bc9dfe5 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Mon, 6 Jan 2025 16:10:29 +0100 Subject: [PATCH 17/65] Present stored quotes using the existing UI design --- .../templates/__messageFormQuote.tpl | 13 ++- .../templates/__messageFormQuoteInline.tpl | 13 ++- ts/WoltLabSuite/Core/Component/Quote/List.ts | 84 +++++++------------ .../Core/Component/Quote/Storage.ts | 31 ++----- .../WoltLabSuite/Core/Component/Quote/List.js | 76 +++++++---------- .../Core/Component/Quote/Storage.js | 23 ++--- .../install/files/style/bbcode/quote.scss | 37 +++++++- 7 files changed, 122 insertions(+), 155 deletions(-) diff --git a/com.woltlab.wcf/templates/__messageFormQuote.tpl b/com.woltlab.wcf/templates/__messageFormQuote.tpl index d4b635e106..16c489f614 100644 --- a/com.woltlab.wcf/templates/__messageFormQuote.tpl +++ b/com.woltlab.wcf/templates/__messageFormQuote.tpl @@ -1,11 +1,8 @@ -{* TODO *} - -
    - -
    +
    +}); + \ No newline at end of file diff --git a/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl b/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl index d4b635e106..16c489f614 100644 --- a/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl +++ b/com.woltlab.wcf/templates/__messageFormQuoteInline.tpl @@ -1,11 +1,8 @@ -{* TODO *} - -
    - -
    +
    +}); + \ No newline at end of file diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 680f905f7f..fa2c558a0b 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -12,8 +12,9 @@ import { listenToCkeditor, dispatchToCkeditor } from "WoltLabSuite/Core/Componen import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; -import { getQuotes, getMessage } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { getQuotes, getMessage, removeQuote } from "WoltLabSuite/Core/Component/Quote/Storage"; import DomUtil from "WoltLabSuite/Core/Dom/Util"; +import { escapeHTML } from "WoltLabSuite/Core/StringUtil"; const quoteLists = new Map(); @@ -43,67 +44,46 @@ class QuoteList { let quotesCount = 0; for (const [key, quotes] of getQuotes()) { const message = getMessage(key)!; - quotesCount += quotes.size; - - // TODO escape values - // TODO create web components??? - const fragment = DomUtil.createFragmentFromHtml(`
    -
    -
    -
    - - -
    -

    - ${message.title} -

    - -
    -
    -
    -
    -
    -
      - ${Array.from(quotes) - .map( - (quote) => `
    • - - - + - - -
      - ${quote.message}
      -
    • `, - ) - .join("")} -
    -
    -
    +
    + ${quote.rawMessage === undefined ? quote.message : quote.rawMessage}
    -
    `); + + `); - // TODO dont query the DOM - fragment.querySelectorAll(".jsInsertQuote").forEach((button) => { - button.addEventListener("click", () => { - // TODO use rawMessage to insert if available otherwise use message + fragment.querySelector('button[data-action="insert"]')!.addEventListener("click", () => { dispatchToCkeditor(this.#editor).insertQuote({ author: message.author, - content: button.closest("li")!.querySelector(".jsQuote")!.innerHTML, - isText: false, + content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, + isText: quote.rawMessage === undefined, link: message.link, }); }); - }); - this.#container.append(fragment); + fragment.querySelector('button[data-action="delete"]')!.addEventListener("click", () => { + removeQuote(key, index); + }); + + this.#container.append(fragment); + }); } if (quotesCount > 0) { diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 5a9413a114..0cc2e255bf 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -29,7 +29,7 @@ interface Quote { } interface StorageData { - quotes: Map>; + quotes: Map; messages: Map; } @@ -76,7 +76,7 @@ export async function saveFullQuote(objectType: string, objectClassName: string, refreshQuoteLists(); } -export function getQuotes(): Map> { +export function getQuotes(): Map { return getStorage().quotes; } @@ -86,17 +86,15 @@ export function getMessage(objectType: string, objectId?: number): Message | und return getStorage().messages.get(key); } -export function removeQuote(objectType: string, objectId: number, quote: Quote): void { +export function removeQuote(key: string, index: number): void { const storage = getStorage(); - - const key = getKey(objectType, objectId); if (!storage.quotes.has(key)) { return; } - storage.quotes.get(key)!.delete(quote); + storage.quotes.get(key)!.splice(index, 1); - if (storage.quotes.get(key)!.size === 0) { + if (storage.quotes.get(key)!.length === 0) { storage.quotes.delete(key); storage.messages.delete(key); } @@ -111,18 +109,11 @@ function storeQuote(objectType: string, message: Message, quote: Quote): void { const key = getKey(objectType, message.objectID); if (!storage.quotes.has(key)) { - storage.quotes.set(key, new Set()); + storage.quotes.set(key, []); } storage.messages.set(key, message); - - if ( - !Array.from(storage.quotes.get(key)!) - .map((q) => q.message) - .includes(quote.message) - ) { - storage.quotes.get(key)!.add(quote); - } + storage.quotes.get(key)!.push(quote); saveStorage(storage); } @@ -137,11 +128,7 @@ function getStorage(): StorageData { } else { return JSON.parse(data, (key, value) => { if (key === "quotes") { - const result = new Map>(value); - for (const [key, setValue] of result) { - result.set(key, new Set(setValue)); - } - return result; + return new Map(value); } else if (key === "messages") { return new Map(value); } @@ -158,7 +145,7 @@ function getKey(objectType: string, objectId: number): string { function saveStorage(data: StorageData) { window.localStorage.setItem( STORAGE_KEY, - JSON.stringify(data, (key, value) => { + JSON.stringify(data, (_key, value) => { if (value instanceof Map) { return Array.from(value.entries()); } else if (value instanceof Set) { 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 d2dd115fc7..6d12ac1fb2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -7,7 +7,7 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1, Util_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/StringUtil"], function (require, exports, tslib_1, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1, Util_1, StringUtil_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getQuoteList = getQuoteList; @@ -36,62 +36,42 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve let quotesCount = 0; for (const [key, quotes] of (0, Storage_1.getQuotes)()) { const message = (0, Storage_1.getMessage)(key); - quotesCount += quotes.size; - // TODO escape values - // TODO create web components??? - const fragment = Util_1.default.createFragmentFromHtml(`
    -
    -
    -
    - - -
    -

    - ${message.title} -

    - -
    -
    -
    -
    -
    -
      - ${Array.from(quotes) - .map((quote) => `
    • - - - + - - -
      - ${quote.message}
      -
    • `) - .join("")} -
    -
    -
    +
    + ${quote.rawMessage === undefined ? quote.message : quote.rawMessage}
    -
    `); - fragment.querySelectorAll(".jsInsertQuote").forEach((button) => { - button.addEventListener("click", () => { - // TODO dont query the DOM - // TODO use rawMessage to insert if available otherwise use message + + `); + fragment.querySelector('button[data-action="insert"]').addEventListener("click", () => { (0, Event_1.dispatchToCkeditor)(this.#editor).insertQuote({ author: message.author, - content: button.closest("li").querySelector(".jsQuote").innerHTML, - isText: false, + content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, + isText: quote.rawMessage === undefined, link: message.link, }); }); + fragment.querySelector('button[data-action="delete"]').addEventListener("click", () => { + (0, Storage_1.removeQuote)(key, index); + }); + this.#container.append(fragment); }); - this.#container.append(fragment); } if (quotesCount > 0) { (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { 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 b71b4f69be..c5cece8d3b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -55,14 +55,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const key = objectId ? getKey(objectType, objectId) : objectType; return getStorage().messages.get(key); } - function removeQuote(objectType, objectId, quote) { + function removeQuote(key, index) { const storage = getStorage(); - const key = getKey(objectType, objectId); if (!storage.quotes.has(key)) { return; } - storage.quotes.get(key).delete(quote); - if (storage.quotes.get(key).size === 0) { + storage.quotes.get(key).splice(index, 1); + if (storage.quotes.get(key).length === 0) { storage.quotes.delete(key); storage.messages.delete(key); } @@ -73,14 +72,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const storage = getStorage(); const key = getKey(objectType, message.objectID); if (!storage.quotes.has(key)) { - storage.quotes.set(key, new Set()); + storage.quotes.set(key, []); } storage.messages.set(key, message); - if (!Array.from(storage.quotes.get(key)) - .map((q) => q.message) - .includes(quote.message)) { - storage.quotes.get(key).add(quote); - } + storage.quotes.get(key).push(quote); saveStorage(storage); } function getStorage() { @@ -94,11 +89,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C else { return JSON.parse(data, (key, value) => { if (key === "quotes") { - const result = new Map(value); - for (const [key, setValue] of result) { - result.set(key, new Set(setValue)); - } - return result; + return new Map(value); } else if (key === "messages") { return new Map(value); @@ -111,7 +102,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C return `${objectType}:${objectId}`; } function saveStorage(data) { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data, (key, value) => { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data, (_key, value) => { if (value instanceof Map) { return Array.from(value.entries()); } diff --git a/wcfsetup/install/files/style/bbcode/quote.scss b/wcfsetup/install/files/style/bbcode/quote.scss index c2e2490cd7..944b6ee8ff 100644 --- a/wcfsetup/install/files/style/bbcode/quote.scss +++ b/wcfsetup/install/files/style/bbcode/quote.scss @@ -8,7 +8,7 @@ display: grid; font-style: normal; grid-template-areas: - "icon title" + "icon title" "content content"; grid-template-columns: 24px auto; margin: 2em 0 1em 0; @@ -79,3 +79,38 @@ margin-bottom: 0 !important; } } + +.quoteBox.quoteBox--tabMenu { + grid-template-areas: + "icon title buttons" + "content content content"; + grid-template-columns: 24px auto min-content; + margin: 0; +} + +.quoteBox.quoteBox--tabMenu + .quoteBox.quoteBox--tabMenu { + margin-top: 10px; +} + +.quoteBoxButtons { + align-self: center; + column-gap: 5px; + display: flex; + grid-area: buttons; + white-space: nowrap; +} + +.quoteBox.quoteBox--tabMenu :is(.quoteBoxIcon, .quoteBoxTitle) { + align-self: center; +} + +.quoteBox.quoteBox--tabMenu .quoteBoxContent { + pointer-events: none !important; +} + +@include screen-xs { + .messageTabMenu:not(.messageTabMenuContent) > .messageTabMenuContent.messageTabMenuContent--quotes.active { + padding-left: 10px; + padding-right: 10px; + } +} From 2a1b94abc5657f571cbf5b6d9756332f0beecec1 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 8 Jan 2025 17:31:51 +0100 Subject: [PATCH 18/65] Use the new tab counters to reflect the number of stored quotes --- .../Core/Component/Message/MessageTabMenu.ts | 37 +++++++++---------- ts/WoltLabSuite/Core/Component/Quote/List.ts | 16 ++++---- .../Core/Component/Message/MessageTabMenu.js | 32 +++++++--------- .../WoltLabSuite/Core/Component/Quote/List.js | 11 ++++-- 4 files changed, 47 insertions(+), 49 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts b/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts index 3b38892d18..390433d0ef 100644 --- a/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts +++ b/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts @@ -65,29 +65,26 @@ class TabMenu { this.#activeTabName = tabName; } - showTab(tabName: string, title?: string): void { - this.#tabs - .filter((element) => element.dataset.name === tabName) - .forEach((element) => { - element.hidden = false; - - // Set new title - if (title) { - element.querySelector("span")!.textContent = title; - } - }); + showTab(tabName: string): void { + const tab = this.#tabs.find((element) => element.dataset.name === tabName); + if (tab === undefined) { + return; + } + + tab.hidden = false; } hideTab(tabName: string): void { - this.#tabs - .filter((element) => element.dataset.name === tabName) - .forEach((element) => { - element.hidden = true; - - if (element.classList.contains("active")) { - this.#closeAllTabs(); - } - }); + const tab = this.#tabs.find((element) => element.dataset.name === tabName); + if (tab === undefined) { + return; + } + + tab.hidden = false; + + if (tab.classList.contains("active")) { + this.#closeAllTabs(); + } } setTabCounter(tabName: string, value: number): void { diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index fa2c558a0b..d206427e26 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -86,15 +86,17 @@ class QuoteList { }); } + const tabMenu = getTabMenu(this.#editorId); + if (tabMenu === undefined) { + throw new Error(`Could not find the tab menu for '${this.#editorId}'.`); + } + + tabMenu.setTabCounter("quotes", quotesCount); + if (quotesCount > 0) { - getTabMenu(this.#editorId)?.showTab( - "quotes", - getPhrase("wcf.message.quote.showQuotes", { - count: quotesCount, - }), - ); + tabMenu.showTab("quotes"); } else { - getTabMenu(this.#editorId)?.hideTab("quotes"); + tabMenu.hideTab("quotes"); } } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js index 7974eff75d..6ed16709b1 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js @@ -53,26 +53,22 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol this.#tabContainers[tabIndex].classList.add("active"); this.#activeTabName = tabName; } - showTab(tabName, title) { - this.#tabs - .filter((element) => element.dataset.name === tabName) - .forEach((element) => { - element.hidden = false; - // Set new title - if (title) { - element.querySelector("span").textContent = title; - } - }); + showTab(tabName) { + const tab = this.#tabs.find((element) => element.dataset.name === tabName); + if (tab === undefined) { + return; + } + tab.hidden = false; } hideTab(tabName) { - this.#tabs - .filter((element) => element.dataset.name === tabName) - .forEach((element) => { - element.hidden = true; - if (element.classList.contains("active")) { - this.#closeAllTabs(); - } - }); + const tab = this.#tabs.find((element) => element.dataset.name === tabName); + if (tab === undefined) { + return; + } + tab.hidden = false; + if (tab.classList.contains("active")) { + this.#closeAllTabs(); + } } setTabCounter(tabName, value) { const tab = this.#tabs.find((element) => element.dataset.name === tabName); 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 6d12ac1fb2..6365343534 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -73,13 +73,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve this.#container.append(fragment); }); } + const tabMenu = (0, MessageTabMenu_1.getTabMenu)(this.#editorId); + if (tabMenu === undefined) { + throw new Error(`Could not find the tab menu for '${this.#editorId}'.`); + } + tabMenu.setTabCounter("quotes", quotesCount); if (quotesCount > 0) { - (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", { - count: quotesCount, - })); + tabMenu.showTab("quotes"); } else { - (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.hideTab("quotes"); + tabMenu.hideTab("quotes"); } } } From bebbe9674c44edf8316db697841fca80e272fa4c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 10:17:43 +0100 Subject: [PATCH 19/65] Fixes an issue where the button area is too large when a word in the content is very long, e.g. an email address. --- wcfsetup/install/files/style/bbcode/quote.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wcfsetup/install/files/style/bbcode/quote.scss b/wcfsetup/install/files/style/bbcode/quote.scss index 944b6ee8ff..0d4b07f314 100644 --- a/wcfsetup/install/files/style/bbcode/quote.scss +++ b/wcfsetup/install/files/style/bbcode/quote.scss @@ -84,7 +84,7 @@ grid-template-areas: "icon title buttons" "content content content"; - grid-template-columns: 24px auto min-content; + grid-template-columns: 24px auto minmax(0, min-content); margin: 0; } From b7a2716302c329fe6bdae325b0e339a6ec0b11eb Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 10:22:04 +0100 Subject: [PATCH 20/65] Set `hidden` to true --- ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts | 2 +- .../js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts b/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts index 390433d0ef..704b238b43 100644 --- a/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts +++ b/ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts @@ -80,7 +80,7 @@ class TabMenu { return; } - tab.hidden = false; + tab.hidden = true; if (tab.classList.contains("active")) { this.#closeAllTabs(); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js index 6ed16709b1..5a1281f532 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js @@ -65,7 +65,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol if (tab === undefined) { return; } - tab.hidden = false; + tab.hidden = true; if (tab.classList.contains("active")) { this.#closeAllTabs(); } From d7414b46230d1b3b129801d2e8f54b4d3725bbd8 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 11:15:09 +0100 Subject: [PATCH 21/65] Save quotes in a set --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 6 ++-- .../Core/Component/Quote/Storage.ts | 30 ++++++++++++++----- .../WoltLabSuite/Core/Component/Quote/List.js | 6 ++-- .../Core/Component/Quote/Storage.js | 24 +++++++++++---- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index d206427e26..da3a417c21 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -44,9 +44,9 @@ class QuoteList { let quotesCount = 0; for (const [key, quotes] of getQuotes()) { const message = getMessage(key)!; - quotesCount += quotes.length; + quotesCount += quotes.size; - quotes.forEach((quote, index) => { + quotes.forEach((quote) => { const fragment = DomUtil.createFragmentFromHtml(`
    @@ -79,7 +79,7 @@ class QuoteList { }); fragment.querySelector('button[data-action="delete"]')!.addEventListener("click", () => { - removeQuote(key, index); + removeQuote(key, quote); }); this.#container.append(fragment); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 0cc2e255bf..1e56efc04f 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -29,7 +29,7 @@ interface Quote { } interface StorageData { - quotes: Map; + quotes: Map>; messages: Map; } @@ -76,7 +76,7 @@ export async function saveFullQuote(objectType: string, objectClassName: string, refreshQuoteLists(); } -export function getQuotes(): Map { +export function getQuotes(): Map> { return getStorage().quotes; } @@ -86,15 +86,19 @@ export function getMessage(objectType: string, objectId?: number): Message | und return getStorage().messages.get(key); } -export function removeQuote(key: string, index: number): void { +export function removeQuote(key: string, quote: Quote): void { const storage = getStorage(); if (!storage.quotes.has(key)) { return; } - storage.quotes.get(key)!.splice(index, 1); + storage.quotes.get(key)!.forEach((q) => { + if (JSON.stringify(q) === JSON.stringify(quote)) { + storage.quotes.get(key)!.delete(q); + } + }); - if (storage.quotes.get(key)!.length === 0) { + if (storage.quotes.get(key)!.size === 0) { storage.quotes.delete(key); storage.messages.delete(key); } @@ -109,11 +113,17 @@ function storeQuote(objectType: string, message: Message, quote: Quote): void { const key = getKey(objectType, message.objectID); if (!storage.quotes.has(key)) { - storage.quotes.set(key, []); + storage.quotes.set(key, new Set()); } storage.messages.set(key, message); - storage.quotes.get(key)!.push(quote); + if ( + !Array.from(storage.quotes.get(key)!) + .map((q) => q.message) + .includes(quote.message) + ) { + storage.quotes.get(key)!.add(quote); + } saveStorage(storage); } @@ -128,7 +138,11 @@ function getStorage(): StorageData { } else { return JSON.parse(data, (key, value) => { if (key === "quotes") { - return new Map(value); + const result = new Map>(value); + for (const [key, setValue] of result) { + result.set(key, new Set(setValue)); + } + return result; } else if (key === "messages") { return new Map(value); } 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 6365343534..584df706d6 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -36,8 +36,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve let quotesCount = 0; for (const [key, quotes] of (0, Storage_1.getQuotes)()) { const message = (0, Storage_1.getMessage)(key); - quotesCount += quotes.length; - quotes.forEach((quote, index) => { + quotesCount += quotes.size; + quotes.forEach((quote) => { const fragment = Util_1.default.createFragmentFromHtml(`
    @@ -68,7 +68,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve }); }); fragment.querySelector('button[data-action="delete"]').addEventListener("click", () => { - (0, Storage_1.removeQuote)(key, index); + (0, Storage_1.removeQuote)(key, quote); }); this.#container.append(fragment); }); 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 c5cece8d3b..18ddf0ba3a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -55,13 +55,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const key = objectId ? getKey(objectType, objectId) : objectType; return getStorage().messages.get(key); } - function removeQuote(key, index) { + function removeQuote(key, quote) { const storage = getStorage(); if (!storage.quotes.has(key)) { return; } - storage.quotes.get(key).splice(index, 1); - if (storage.quotes.get(key).length === 0) { + storage.quotes.get(key).forEach((q) => { + if (JSON.stringify(q) === JSON.stringify(quote)) { + storage.quotes.get(key).delete(q); + } + }); + if (storage.quotes.get(key).size === 0) { storage.quotes.delete(key); storage.messages.delete(key); } @@ -72,10 +76,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const storage = getStorage(); const key = getKey(objectType, message.objectID); if (!storage.quotes.has(key)) { - storage.quotes.set(key, []); + storage.quotes.set(key, new Set()); } storage.messages.set(key, message); - storage.quotes.get(key).push(quote); + if (!Array.from(storage.quotes.get(key)) + .map((q) => q.message) + .includes(quote.message)) { + storage.quotes.get(key).add(quote); + } saveStorage(storage); } function getStorage() { @@ -89,7 +97,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C else { return JSON.parse(data, (key, value) => { if (key === "quotes") { - return new Map(value); + const result = new Map(value); + for (const [key, setValue] of result) { + result.set(key, new Set(setValue)); + } + return result; } else if (key === "messages") { return new Map(value); From ae906e372dc433804bcd86d2e342da4d3ddacbac Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 11:24:44 +0100 Subject: [PATCH 22/65] Use `JSON.stringify` to compare --- ts/WoltLabSuite/Core/Component/Quote/Storage.ts | 4 ++-- .../files/js/WoltLabSuite/Core/Component/Quote/Storage.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 1e56efc04f..34e868ff8e 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -119,8 +119,8 @@ function storeQuote(objectType: string, message: Message, quote: Quote): void { storage.messages.set(key, message); if ( !Array.from(storage.quotes.get(key)!) - .map((q) => q.message) - .includes(quote.message) + .map((q) => JSON.stringify(q)) + .includes(JSON.stringify(quote)) ) { storage.quotes.get(key)!.add(quote); } 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 18ddf0ba3a..18d3fac0d8 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -80,8 +80,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C } storage.messages.set(key, message); if (!Array.from(storage.quotes.get(key)) - .map((q) => q.message) - .includes(quote.message)) { + .map((q) => JSON.stringify(q)) + .includes(JSON.stringify(quote))) { storage.quotes.get(key).add(quote); } saveStorage(storage); From 7eb03e62f36cab2c62ddb694d0451d1536e45592 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 11:37:40 +0100 Subject: [PATCH 23/65] Insert quote by pressing the button --- .../Core/Component/Quote/Message.ts | 25 ++++++-- .../Core/Component/Quote/Storage.ts | 61 ++++++++++++------- .../Core/Component/Quote/Message.js | 24 ++++++-- .../Core/Component/Quote/Storage.js | 22 ++++--- 4 files changed, 94 insertions(+), 38 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 92fb91042c..255b8a0ba1 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -15,6 +15,7 @@ import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment"; import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; import { saveQuote, saveFullQuote } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; +import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; interface Container { element: HTMLElement; @@ -66,8 +67,16 @@ export function registerContainer( promiseMutex(async (event: MouseEvent) => { event.preventDefault(); - await saveFullQuote(objectType, className, ~~container.dataset.objectId!); - //TODO insert into `activeEditor` + const quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!); + + if (activeEditor !== undefined) { + dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ + author: quoteMessage.author, + content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, + isText: quoteMessage.rawMessage === undefined, + link: quoteMessage.link, + }); + } }), ); }); @@ -108,13 +117,21 @@ function setup() { buttonSaveAndInsertQuote.addEventListener( "click", promiseMutex(async () => { - await saveQuote( + const quoteMessage = await saveQuote( selectedMessage!.container.objectType, selectedMessage!.container.objectId, selectedMessage!.container.className, selectedMessage!.message, ); - //TODO insert into `activeEditor` + + if (activeEditor !== undefined) { + dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ + author: quoteMessage.author, + content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, + isText: quoteMessage.rawMessage === undefined, + link: quoteMessage.link, + }); + } removeSelection(); }), diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 34e868ff8e..82e9861d80 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -35,11 +35,15 @@ interface StorageData { const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; -export async function saveQuote(objectType: string, objectId: number, objectClassName: string, message: string) { +export async function saveQuote( + objectType: string, + objectId: number, + objectClassName: string, + message: string, +): Promise { const result = await messageAuthor(objectClassName, objectId); if (!result.ok) { - // TODO error handling - return; + throw new Error("Error fetching author data"); } storeQuote(objectType, result.value, { @@ -47,33 +51,46 @@ export async function saveQuote(objectType: string, objectId: number, objectClas }); refreshQuoteLists(); + + return { + ...result.value, + message, + }; } -export async function saveFullQuote(objectType: string, objectClassName: string, objectId: number) { +export async function saveFullQuote( + objectType: string, + objectClassName: string, + objectId: number, +): Promise { const result = await renderQuote(objectType, objectClassName, objectId); if (!result.ok) { - // TODO error handling - return; + throw new Error("Error fetching quote data"); } - storeQuote( - objectType, - { - 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, - }, - { - message: result.value.message!, - rawMessage: result.value.rawMessage!, - }, - ); + 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, + }; + + const quote = { + message: result.value.message!, + rawMessage: result.value.rawMessage!, + }; + + storeQuote(objectType, message, quote); refreshQuoteLists(); + + return { + ...message, + ...quote, + }; } export function getQuotes(): Map> { 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 72f49b9485..c10c358b3a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -7,7 +7,7 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Helper/PromiseMutex"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1, Storage_1, PromiseMutex_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Component/Ckeditor/Event"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1, Storage_1, PromiseMutex_1, Event_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerContainer = registerContainer; @@ -37,8 +37,15 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui container.classList.add("jsQuoteMessageContainer"); container.querySelector(".jsQuoteMessage")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { event.preventDefault(); - await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); - //TODO insert into `activeEditor` + const quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); + 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, + link: quoteMessage.link, + }); + } })); }); } @@ -63,8 +70,15 @@ 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 () => { - await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); - //TODO insert into `activeEditor` + const 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, + content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, + isText: quoteMessage.rawMessage === undefined, + link: quoteMessage.link, + }); + } removeSelection(); })); copyQuote.appendChild(buttonSaveAndInsertQuote); 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 18d3fac0d8..18dd52b850 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -20,21 +20,23 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C async function saveQuote(objectType, objectId, objectClassName, message) { const result = await (0, Author_1.messageAuthor)(objectClassName, objectId); if (!result.ok) { - // TODO error handling - return; + throw new Error("Error fetching author data"); } storeQuote(objectType, result.value, { message, }); (0, List_1.refreshQuoteLists)(); + return { + ...result.value, + message, + }; } async function saveFullQuote(objectType, objectClassName, objectId) { const result = await (0, RenderQuote_1.renderQuote)(objectType, objectClassName, objectId); if (!result.ok) { - // TODO error handling - return; + throw new Error("Error fetching quote data"); } - storeQuote(objectType, { + const message = { objectID: result.value.objectID, time: result.value.time, title: result.value.title, @@ -42,11 +44,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C authorID: result.value.authorID, author: result.value.author, avatar: result.value.avatar, - }, { + }; + const quote = { message: result.value.message, rawMessage: result.value.rawMessage, - }); + }; + storeQuote(objectType, message, quote); (0, List_1.refreshQuoteLists)(); + return { + ...message, + ...quote, + }; } function getQuotes() { return getStorage().quotes; From 0329f39b0e287bce3f314f9fc0e83637daecf0f5 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 14:40:09 +0100 Subject: [PATCH 24/65] Remove quotes from the storage as soon as the message has been successfully created by quick reply --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 8 +- .../Core/Component/Quote/Message.ts | 6 +- .../Core/Component/Quote/Storage.ts | 82 +++++++++++++------ ts/WoltLabSuite/Core/Ui/Message/Reply.ts | 3 + .../WoltLabSuite/Core/Component/Quote/List.js | 5 +- .../Core/Component/Quote/Message.js | 2 + .../Core/Component/Quote/Storage.js | 60 ++++++++++---- .../js/WoltLabSuite/Core/Ui/Message/Reply.js | 3 +- 8 files changed, 119 insertions(+), 50 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index da3a417c21..b0f07693a5 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -12,7 +12,7 @@ import { listenToCkeditor, dispatchToCkeditor } from "WoltLabSuite/Core/Componen import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; -import { getQuotes, getMessage, removeQuote } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { getQuotes, getMessage, removeQuote, markQuoteAsUsed } from "WoltLabSuite/Core/Component/Quote/Storage"; import DomUtil from "WoltLabSuite/Core/Dom/Util"; import { escapeHTML } from "WoltLabSuite/Core/StringUtil"; @@ -46,7 +46,7 @@ class QuoteList { const message = getMessage(key)!; quotesCount += quotes.size; - quotes.forEach((quote) => { + quotes.forEach((quote, uuid) => { const fragment = DomUtil.createFragmentFromHtml(`
    @@ -70,6 +70,8 @@ class QuoteList { `); fragment.querySelector('button[data-action="insert"]')!.addEventListener("click", () => { + markQuoteAsUsed(this.#editorId, uuid); + dispatchToCkeditor(this.#editor).insertQuote({ author: message.author, content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, @@ -79,7 +81,7 @@ class QuoteList { }); fragment.querySelector('button[data-action="delete"]')!.addEventListener("click", () => { - removeQuote(key, quote); + removeQuote(key, uuid); }); this.#container.append(fragment); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 255b8a0ba1..84c08f8f10 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -13,7 +13,7 @@ import { getPhrase } from "WoltLabSuite/Core/Language"; import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment"; import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; -import { saveQuote, saveFullQuote } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { saveQuote, saveFullQuote, markQuoteAsUsed } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; @@ -76,6 +76,8 @@ export function registerContainer( isText: quoteMessage.rawMessage === undefined, link: quoteMessage.link, }); + + markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); } }), ); @@ -131,6 +133,8 @@ function setup() { isText: quoteMessage.rawMessage === undefined, link: quoteMessage.link, }); + + markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); } removeSelection(); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 82e9861d80..d5c6586ca4 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -29,24 +29,25 @@ interface Quote { } interface StorageData { - quotes: Map>; + quotes: Map>; messages: Map; } const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; +const usedQuotes = new Map>(); export async function saveQuote( objectType: string, objectId: number, objectClassName: string, message: string, -): Promise { +): Promise { const result = await messageAuthor(objectClassName, objectId); if (!result.ok) { throw new Error("Error fetching author data"); } - storeQuote(objectType, result.value, { + const uuid = storeQuote(objectType, result.value, { message, }); @@ -55,6 +56,7 @@ export async function saveQuote( return { ...result.value, message, + uuid, }; } @@ -62,7 +64,7 @@ export async function saveFullQuote( objectType: string, objectClassName: string, objectId: number, -): Promise { +): Promise { const result = await renderQuote(objectType, objectClassName, objectId); if (!result.ok) { throw new Error("Error fetching quote data"); @@ -83,17 +85,18 @@ export async function saveFullQuote( rawMessage: result.value.rawMessage!, }; - storeQuote(objectType, message, quote); + const uuid = storeQuote(objectType, message, quote); refreshQuoteLists(); return { ...message, ...quote, + uuid, }; } -export function getQuotes(): Map> { +export function getQuotes(): Map> { return getStorage().quotes; } @@ -103,17 +106,13 @@ export function getMessage(objectType: string, objectId?: number): Message | und return getStorage().messages.get(key); } -export function removeQuote(key: string, quote: Quote): void { +export function removeQuote(key: string, uuid: string): void { const storage = getStorage(); if (!storage.quotes.has(key)) { return; } - storage.quotes.get(key)!.forEach((q) => { - if (JSON.stringify(q) === JSON.stringify(quote)) { - storage.quotes.get(key)!.delete(q); - } - }); + storage.quotes.get(key)!.delete(uuid); if (storage.quotes.get(key)!.size === 0) { storage.quotes.delete(key); @@ -125,24 +124,58 @@ export function removeQuote(key: string, quote: Quote): void { refreshQuoteLists(); } -function storeQuote(objectType: string, message: Message, quote: Quote): void { +export function markQuoteAsUsed(editorId: string, uuid: string): void { + if (!usedQuotes.has(editorId)) { + usedQuotes.set(editorId, new Set()); + } + + usedQuotes.get(editorId)!.add(uuid); +} + +export function clearQuotesForEditor(editorId: string): void { + const storage = getStorage(); + + usedQuotes.get(editorId)?.forEach((uuid) => { + for (const quotes of storage.quotes.values()) { + quotes.delete(uuid); + } + }); + + usedQuotes.delete(editorId); + + for (const [key, quotes] of storage.quotes) { + if (quotes.size === 0) { + storage.quotes.delete(key); + storage.messages.delete(key); + } + } + + saveStorage(storage); + refreshQuoteLists(); +} + +function storeQuote(objectType: string, message: Message, quote: Quote): string { const storage = getStorage(); const key = getKey(objectType, message.objectID); if (!storage.quotes.has(key)) { - storage.quotes.set(key, new Set()); + storage.quotes.set(key, new Map()); } storage.messages.set(key, message); - if ( - !Array.from(storage.quotes.get(key)!) - .map((q) => JSON.stringify(q)) - .includes(JSON.stringify(quote)) - ) { - storage.quotes.get(key)!.add(quote); + + const uuid = Core.getUuid(); + for (const [uuid, q] of storage.quotes.get(key)!) { + if (JSON.stringify(q) === JSON.stringify(quote)) { + return uuid; + } } + storage.quotes.get(key)!.set(uuid, quote); + saveStorage(storage); + + return uuid; } function getStorage(): StorageData { @@ -155,10 +188,11 @@ function getStorage(): StorageData { } else { return JSON.parse(data, (key, value) => { if (key === "quotes") { - const result = new Map>(value); - for (const [key, setValue] of result) { - result.set(key, new Set(setValue)); + const result = new Map>(value); + for (const [key, quotes] of result) { + result.set(key, new Map(quotes)); } + return result; } else if (key === "messages") { return new Map(value); @@ -179,8 +213,6 @@ function saveStorage(data: StorageData) { JSON.stringify(data, (_key, value) => { if (value instanceof Map) { return Array.from(value.entries()); - } else if (value instanceof Set) { - return Array.from(value); } return value; diff --git a/ts/WoltLabSuite/Core/Ui/Message/Reply.ts b/ts/WoltLabSuite/Core/Ui/Message/Reply.ts index e8a915958d..ba3cf6bee6 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Reply.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Reply.ts @@ -22,6 +22,7 @@ import * as UiScroll from "../Scroll"; import { CKEditor, getCkeditor } from "../../Component/Ckeditor"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; +import { clearQuotesForEditor } from "WoltLabSuite/Core/Component/Quote/Storage"; interface MessageReplyOptions { ajax: { @@ -386,6 +387,8 @@ class UiMessageReply { this._guestDialogId = guestDialogId; } else { + clearQuotesForEditor(this._textarea.id); + this._insertMessage(data); if (!User.userId) { 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 584df706d6..f1915f35d9 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -37,7 +37,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve for (const [key, quotes] of (0, Storage_1.getQuotes)()) { const message = (0, Storage_1.getMessage)(key); quotesCount += quotes.size; - quotes.forEach((quote) => { + quotes.forEach((quote, uuid) => { const fragment = Util_1.default.createFragmentFromHtml(`
    @@ -60,6 +60,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
    `); fragment.querySelector('button[data-action="insert"]').addEventListener("click", () => { + (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, @@ -68,7 +69,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve }); }); fragment.querySelector('button[data-action="delete"]').addEventListener("click", () => { - (0, Storage_1.removeQuote)(key, quote); + (0, Storage_1.removeQuote)(key, uuid); }); this.#container.append(fragment); }); 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 c10c358b3a..45b858c0f4 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -45,6 +45,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui isText: quoteMessage.rawMessage === undefined, link: quoteMessage.link, }); + (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); } })); }); @@ -78,6 +79,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui isText: quoteMessage.rawMessage === undefined, link: quoteMessage.link, }); + (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.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 18dd52b850..46aa27730a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -15,20 +15,24 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C exports.getQuotes = getQuotes; exports.getMessage = getMessage; exports.removeQuote = removeQuote; + exports.markQuoteAsUsed = markQuoteAsUsed; + exports.clearQuotesForEditor = clearQuotesForEditor; Core = tslib_1.__importStar(Core); const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; + const usedQuotes = new Map(); async function saveQuote(objectType, objectId, objectClassName, message) { const result = await (0, Author_1.messageAuthor)(objectClassName, objectId); if (!result.ok) { throw new Error("Error fetching author data"); } - storeQuote(objectType, result.value, { + const uuid = storeQuote(objectType, result.value, { message, }); (0, List_1.refreshQuoteLists)(); return { ...result.value, message, + uuid, }; } async function saveFullQuote(objectType, objectClassName, objectId) { @@ -49,11 +53,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C message: result.value.message, rawMessage: result.value.rawMessage, }; - storeQuote(objectType, message, quote); + const uuid = storeQuote(objectType, message, quote); (0, List_1.refreshQuoteLists)(); return { ...message, ...quote, + uuid, }; } function getQuotes() { @@ -63,16 +68,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const key = objectId ? getKey(objectType, objectId) : objectType; return getStorage().messages.get(key); } - function removeQuote(key, quote) { + function removeQuote(key, uuid) { const storage = getStorage(); if (!storage.quotes.has(key)) { return; } - storage.quotes.get(key).forEach((q) => { - if (JSON.stringify(q) === JSON.stringify(quote)) { - storage.quotes.get(key).delete(q); - } - }); + storage.quotes.get(key).delete(uuid); if (storage.quotes.get(key).size === 0) { storage.quotes.delete(key); storage.messages.delete(key); @@ -80,19 +81,45 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C saveStorage(storage); (0, List_1.refreshQuoteLists)(); } + function markQuoteAsUsed(editorId, uuid) { + if (!usedQuotes.has(editorId)) { + usedQuotes.set(editorId, new Set()); + } + usedQuotes.get(editorId).add(uuid); + } + function clearQuotesForEditor(editorId) { + const storage = getStorage(); + usedQuotes.get(editorId)?.forEach((uuid) => { + for (const quotes of storage.quotes.values()) { + quotes.delete(uuid); + } + }); + usedQuotes.delete(editorId); + for (const [key, quotes] of storage.quotes) { + if (quotes.size === 0) { + storage.quotes.delete(key); + storage.messages.delete(key); + } + } + saveStorage(storage); + (0, List_1.refreshQuoteLists)(); + } function storeQuote(objectType, message, quote) { const storage = getStorage(); const key = getKey(objectType, message.objectID); if (!storage.quotes.has(key)) { - storage.quotes.set(key, new Set()); + storage.quotes.set(key, new Map()); } storage.messages.set(key, message); - if (!Array.from(storage.quotes.get(key)) - .map((q) => JSON.stringify(q)) - .includes(JSON.stringify(quote))) { - storage.quotes.get(key).add(quote); + const uuid = Core.getUuid(); + for (const [uuid, q] of storage.quotes.get(key)) { + if (JSON.stringify(q) === JSON.stringify(quote)) { + return uuid; + } } + storage.quotes.get(key).set(uuid, quote); saveStorage(storage); + return uuid; } function getStorage() { const data = window.localStorage.getItem(STORAGE_KEY); @@ -106,8 +133,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C return JSON.parse(data, (key, value) => { if (key === "quotes") { const result = new Map(value); - for (const [key, setValue] of result) { - result.set(key, new Set(setValue)); + for (const [key, quotes] of result) { + result.set(key, new Map(quotes)); } return result; } @@ -126,9 +153,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C if (value instanceof Map) { return Array.from(value.entries()); } - else if (value instanceof Set) { - return Array.from(value); - } return value; })); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js index cb9030319b..67712bfb04 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util", "../Dialog", "../Notification", "../../User", "../../Controller/Captcha", "../Scroll", "../../Component/Ckeditor", "WoltLabSuite/Core/Component/Ckeditor/Event"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1, Dialog_1, UiNotification, User_1, Captcha_1, UiScroll, Ckeditor_1, Event_1) { +define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util", "../Dialog", "../Notification", "../../User", "../../Controller/Captcha", "../Scroll", "../../Component/Ckeditor", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Quote/Storage"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1, Dialog_1, UiNotification, User_1, Captcha_1, UiScroll, Ckeditor_1, Event_1, Storage_1) { "use strict"; Ajax = tslib_1.__importStar(Ajax); Core = tslib_1.__importStar(Core); @@ -306,6 +306,7 @@ define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/ this._guestDialogId = guestDialogId; } else { + (0, Storage_1.clearQuotesForEditor)(this._textarea.id); this._insertMessage(data); if (!User_1.default.userId) { Dialog_1.default.close(data.returnValues.guestDialogID); From 046289955097516329a4b656b238cb4670c9a1c1 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 13 Jan 2025 14:41:21 +0100 Subject: [PATCH 25/65] Generate uuid only if the quote is not already saved --- ts/WoltLabSuite/Core/Component/Quote/Storage.ts | 2 +- .../files/js/WoltLabSuite/Core/Component/Quote/Storage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index d5c6586ca4..e686fff63c 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -164,13 +164,13 @@ function storeQuote(objectType: string, message: Message, quote: Quote): string storage.messages.set(key, message); - const uuid = Core.getUuid(); for (const [uuid, q] of storage.quotes.get(key)!) { if (JSON.stringify(q) === JSON.stringify(quote)) { return uuid; } } + const uuid = Core.getUuid(); storage.quotes.get(key)!.set(uuid, quote); saveStorage(storage); 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 46aa27730a..f92c90c9e8 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -111,12 +111,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C storage.quotes.set(key, new Map()); } storage.messages.set(key, message); - const uuid = Core.getUuid(); for (const [uuid, q] of storage.quotes.get(key)) { if (JSON.stringify(q) === JSON.stringify(quote)) { return uuid; } } + const uuid = Core.getUuid(); storage.quotes.get(key).set(uuid, quote); saveStorage(storage); return uuid; From aef20c1778e41546b6b472ffbb68f03cc1b3a079 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 08:04:02 +0100 Subject: [PATCH 26/65] Define `$quoteManager` for backwards compatibility --- com.woltlab.wcf/templates/shared_messageQuoteManager.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl b/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl index 2460636c22..55d98b4947 100644 --- a/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl +++ b/com.woltlab.wcf/templates/shared_messageQuoteManager.tpl @@ -1 +1,2 @@ {* Deprecated since 6.2 *} +var $quoteManager = undefined; From 0dbe5ec016a511faa8e2c038e46b6a2b1b1c8a72 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 09:49:36 +0100 Subject: [PATCH 27/65] Show active quoted messages --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 15 +++++++-- .../Core/Component/Quote/Message.ts | 31 +++++++++++++++++-- .../Core/Component/Quote/Storage.ts | 24 +++++++++++++- .../WoltLabSuite/Core/Component/Quote/List.js | 5 ++- .../Core/Component/Quote/Message.js | 19 ++++++++++-- .../Core/Component/Quote/Storage.js | 19 ++++++++++++ 6 files changed, 103 insertions(+), 10 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index b0f07693a5..d015171989 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -11,8 +11,14 @@ import { listenToCkeditor, dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu"; import { getPhrase } from "WoltLabSuite/Core/Language"; -import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; -import { getQuotes, getMessage, removeQuote, markQuoteAsUsed } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { setActiveEditor, removeQuoteStatus } from "WoltLabSuite/Core/Component/Quote/Message"; +import { + getQuotes, + getMessage, + removeQuote, + markQuoteAsUsed, + getUsedQuotes, +} from "WoltLabSuite/Core/Component/Quote/Storage"; import DomUtil from "WoltLabSuite/Core/Dom/Util"; import { escapeHTML } from "WoltLabSuite/Core/StringUtil"; @@ -82,6 +88,7 @@ class QuoteList { fragment.querySelector('button[data-action="delete"]')!.addEventListener("click", () => { removeQuote(key, uuid); + removeQuoteStatus(key); }); this.#container.append(fragment); @@ -128,7 +135,9 @@ export function setup(editorId: string): void { quoteLists.set(editorId, new QuoteList(editorId, editor)); } - setActiveEditor(ckeditor, ckeditor.features.quoteBlock); + if (ckeditor.isVisible()) { + setActiveEditor(ckeditor, ckeditor.features.quoteBlock); + } ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => { if (isFocused) { diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 84c08f8f10..414e675083 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -13,7 +13,13 @@ import { getPhrase } from "WoltLabSuite/Core/Language"; import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment"; import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor"; -import { saveQuote, saveFullQuote, markQuoteAsUsed } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { + saveQuote, + saveFullQuote, + markQuoteAsUsed, + isFullQuoted, + getKey, +} from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; @@ -33,6 +39,7 @@ let selectedMessage: }; const containers = new Map(); +const quoteMessageButtons = new Map(); let activeMessageId = ""; let activeEditor: CKEditor | undefined = undefined; let timerSelectionChange: number | undefined = undefined; @@ -47,12 +54,14 @@ export function registerContainer( ): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); + const objectId = ~~container.dataset.objectId!; + containers.set(id, { element: container, messageBodySelector: messageBodySelector, objectType: objectType, className: className, - objectId: ~~container.dataset.objectId!, + objectId: objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -62,7 +71,17 @@ export function registerContainer( container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); - container.querySelector(".jsQuoteMessage")?.addEventListener( + const quoteMessage = container.querySelector(".jsQuoteMessage"); + const quoteMessageButton = quoteMessage?.querySelector(".button"); + if (quoteMessageButton) { + quoteMessageButtons.set(getKey(objectType, objectId), quoteMessageButton); + + if (isFullQuoted(objectType, objectId)) { + quoteMessageButton.classList.add("active"); + } + } + + quoteMessage?.addEventListener( "click", promiseMutex(async (event: MouseEvent) => { event.preventDefault(); @@ -79,6 +98,8 @@ export function registerContainer( markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); } + + quoteMessageButton!.classList.add("active"); }), ); }); @@ -90,6 +111,10 @@ export function setActiveEditor(editor?: CKEditor, supportDirectInsert: boolean activeEditor = editor; } +export function removeQuoteStatus(key: string): void { + quoteMessageButtons.get(key)?.classList.remove("active"); +} + function setup() { copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy"); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index e686fff63c..ad00fd335a 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -132,6 +132,10 @@ export function markQuoteAsUsed(editorId: string, uuid: string): void { usedQuotes.get(editorId)!.add(uuid); } +export function getUsedQuotes(editorId: string): Set { + return usedQuotes.get(editorId) ?? new Set(); +} + export function clearQuotesForEditor(editorId: string): void { const storage = getStorage(); @@ -154,6 +158,24 @@ export function clearQuotesForEditor(editorId: string): void { refreshQuoteLists(); } +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(); @@ -203,7 +225,7 @@ function getStorage(): StorageData { } } -function getKey(objectType: string, objectId: number): string { +export function getKey(objectType: string, objectId: number): string { return `${objectType}:${objectId}`; } 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 f1915f35d9..c632a02abe 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -70,6 +70,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve }); fragment.querySelector('button[data-action="delete"]').addEventListener("click", () => { (0, Storage_1.removeQuote)(key, uuid); + (0, Message_1.removeQuoteStatus)(key); }); this.#container.append(fragment); }); @@ -107,7 +108,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, editor)); } - (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); + if (ckeditor.isVisible()) { + (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); + } ckeditor.focusTracker.on("change:isFocused", (_evt, _name, isFocused) => { if (isFocused) { (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js index 45b858c0f4..83517b1330 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -12,9 +12,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui Object.defineProperty(exports, "__esModule", { value: true }); exports.registerContainer = registerContainer; exports.setActiveEditor = setActiveEditor; + exports.removeQuoteStatus = removeQuoteStatus; Util_1 = tslib_1.__importDefault(Util_1); let selectedMessage; const containers = new Map(); + const quoteMessageButtons = new Map(); let activeMessageId = ""; let activeEditor = undefined; let timerSelectionChange = undefined; @@ -23,19 +25,28 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui function registerContainer(containerSelector, messageBodySelector, className, objectType) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); + const objectId = ~~container.dataset.objectId; containers.set(id, { element: container, messageBodySelector: messageBodySelector, objectType: objectType, className: className, - objectId: ~~container.dataset.objectId, + objectId: objectId, }); if (container.classList.contains("jsInvalidQuoteTarget")) { return; } container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); - container.querySelector(".jsQuoteMessage")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { + const quoteMessage = container.querySelector(".jsQuoteMessage"); + const quoteMessageButton = quoteMessage?.querySelector(".button"); + if (quoteMessageButton) { + quoteMessageButtons.set((0, Storage_1.getKey)(objectType, objectId), quoteMessageButton); + if ((0, Storage_1.isFullQuoted)(objectType, objectId)) { + quoteMessageButton.classList.add("active"); + } + } + quoteMessage?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { event.preventDefault(); const quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); if (activeEditor !== undefined) { @@ -47,6 +58,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui }); (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); } + quoteMessageButton.classList.add("active"); })); }); } @@ -54,6 +66,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert").hidden = !supportDirectInsert; activeEditor = editor; } + function removeQuoteStatus(key) { + quoteMessageButtons.get(key)?.classList.remove("active"); + } function setup() { copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy"); const buttonSaveQuote = document.createElement("button"); 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 f92c90c9e8..f086797376 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -16,7 +16,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C exports.getMessage = getMessage; exports.removeQuote = removeQuote; exports.markQuoteAsUsed = markQuoteAsUsed; + exports.getUsedQuotes = getUsedQuotes; exports.clearQuotesForEditor = clearQuotesForEditor; + exports.isFullQuoted = isFullQuoted; + exports.getKey = getKey; Core = tslib_1.__importStar(Core); const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map(); @@ -87,6 +90,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C } usedQuotes.get(editorId).add(uuid); } + function getUsedQuotes(editorId) { + return usedQuotes.get(editorId) ?? new Set(); + } function clearQuotesForEditor(editorId) { const storage = getStorage(); usedQuotes.get(editorId)?.forEach((uuid) => { @@ -104,6 +110,19 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C saveStorage(storage); (0, List_1.refreshQuoteLists)(); } + 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); From f069c5f4f067dd38468c494eb3997ba1088aef4b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 09:50:16 +0100 Subject: [PATCH 28/65] Send the uuids from quoted messages to remove them from the next request --- ts/WoltLabSuite/Core/Component/Quote/List.ts | 14 ++++++++++++++ .../js/WoltLabSuite/Core/Component/Quote/List.js | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index d015171989..35874e1579 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -37,6 +37,10 @@ class QuoteList { throw new Error(`The quotes container for '${editorId}' does not exist.`); } + this.#editor.closest("form")?.addEventListener("submit", () => { + this.#formSubmitted(); + }); + window.addEventListener("storage", () => { this.renderQuotes(); }); @@ -108,6 +112,16 @@ class QuoteList { tabMenu.hideTab("quotes"); } } + + #formSubmitted(): void { + const formSubmit = this.#editor.closest("form")!.querySelector(".formSubmit")!; + + getUsedQuotes(this.#editorId).forEach((uuid) => { + formSubmit.append( + DomUtil.createFragmentFromHtml(``), + ); + }); + } } export function getQuoteList(editorId: string): QuoteList | undefined { 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 c632a02abe..022c7b951b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -26,6 +26,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve if (this.#container === null) { throw new Error(`The quotes container for '${editorId}' does not exist.`); } + this.#editor.closest("form")?.addEventListener("submit", () => { + this.#formSubmitted(); + }); window.addEventListener("storage", () => { this.renderQuotes(); }); @@ -87,6 +90,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve tabMenu.hideTab("quotes"); } } + #formSubmitted() { + const formSubmit = this.#editor.closest("form").querySelector(".formSubmit"); + (0, Storage_1.getUsedQuotes)(this.#editorId).forEach((uuid) => { + formSubmit.append(Util_1.default.createFragmentFromHtml(``)); + }); + } } function getQuoteList(editorId) { return quoteLists.get(editorId); From 6309ec9059f4f797889681af8235204a24886979 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 09:52:34 +0100 Subject: [PATCH 29/65] Mark `IMessageQuoteHandler` as deprecated --- .../system/message/quote/AbstractMessageQuoteHandler.class.php | 2 ++ .../lib/system/message/quote/IMessageQuoteHandler.class.php | 2 ++ 2 files changed, 4 insertions(+) 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 35981c01c1..7534f6f2af 100644 --- a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php @@ -12,6 +12,8 @@ * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 */ abstract class AbstractMessageQuoteHandler extends SingletonFactory implements IMessageQuoteHandler { 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 a302dda0e1..c86d11b037 100644 --- a/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php @@ -8,6 +8,8 @@ * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 */ interface IMessageQuoteHandler { From 88b789912a0fc50ba458dc3a0f1f03ee2076967e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:30:43 +0100 Subject: [PATCH 30/65] Delete the quotes saved between the next request after successfully sending the form --- com.woltlab.wcf/coreObject.xml | 3 + .../templates/headIncludeJavaScript.tpl | 3 +- .../Core/Api/Messages/ResetRemovalQuotes.ts | 24 + ts/WoltLabSuite/Core/BootstrapFrontend.ts | 6 + .../Core/Component/Quote/Storage.ts | 16 + .../Core/Api/Messages/ResetRemovalQuotes.js | 24 + .../js/WoltLabSuite/Core/BootstrapFrontend.js | 17 +- .../Core/Component/Quote/Storage.js | 14 +- .../messages/ResetRemovalQuotes.class.php | 30 ++ .../quote/MessageQuoteManager.class.php | 419 ++++-------------- 10 files changed, 203 insertions(+), 353 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.js create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/messages/ResetRemovalQuotes.class.php diff --git a/com.woltlab.wcf/coreObject.xml b/com.woltlab.wcf/coreObject.xml index 835d5b2f4e..b251010167 100644 --- a/com.woltlab.wcf/coreObject.xml +++ b/com.woltlab.wcf/coreObject.xml @@ -55,6 +55,9 @@ wcf\system\file\upload\UploadHandler + + wcf\system\message\quote\MessageQuoteManager + diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index 4c72ec8b21..c3c8e91ffe 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -101,7 +101,8 @@ window.addEventListener('pageshow', function(event) { {event name='javascriptShareButtonProviders'} ], {/if} - styleChanger: {if $__wcf->getStyleHandler()->showStyleChanger()}true{else}false{/if} + styleChanger: {if $__wcf->getStyleHandler()->showStyleChanger()}true{else}false{/if}, + {if $__wcf->user->userID}removeQuotes: [{implode from=$__wcf->getMessageQuoteManager()->getRemoveQuoteIDs() item=uuid}'{$uuid|encodeJS}'{/implode}],{/if} }); }); diff --git a/ts/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.ts b/ts/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.ts new file mode 100644 index 0000000000..9f88766740 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.ts @@ -0,0 +1,24 @@ +/** + * Requests to reset the removal quotes. + * + * @author Olaf Braun + * @copyright 2001-2025 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"; + +export async function resetRemovalQuotes(): Promise> { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/reset-removal-quotes"); + + try { + await prepareRequest(url).post().fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/BootstrapFrontend.ts b/ts/WoltLabSuite/Core/BootstrapFrontend.ts index efc594736f..b7ccc37040 100644 --- a/ts/WoltLabSuite/Core/BootstrapFrontend.ts +++ b/ts/WoltLabSuite/Core/BootstrapFrontend.ts @@ -37,6 +37,7 @@ interface BootstrapOptions { executeCronjobs: string | undefined; shareButtonProviders?: ShareProvider[]; styleChanger: boolean; + removeQuotes?: string[]; } /** @@ -86,6 +87,11 @@ export function setup(options: BootstrapOptions): void { enableMobileMenu: true, pageMenuMainProvider: new UiPageMenuMainFrontend(), }); + + if (options.removeQuotes?.length) { + void import("./Component/Quote/Storage").then(({ removeQuotes }) => removeQuotes(options.removeQuotes!)); + } + UiPageHeaderMenu.init(); if (options.styleChanger) { diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index ad00fd335a..806b45601a 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -12,6 +12,7 @@ import * as Core from "WoltLabSuite/Core/Core"; import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; import { messageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; import { refreshQuoteLists } from "WoltLabSuite/Core/Component/Quote/List"; +import { resetRemovalQuotes } from "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes"; interface Message { objectID: number; @@ -106,6 +107,21 @@ export function getMessage(objectType: string, objectId?: number): Message | und return getStorage().messages.get(key); } +export function removeQuotes(uuids: string[]): void { + const storage = getStorage(); + + for (const uuid of uuids) { + for (const quotes of storage.quotes.values()) { + quotes.delete(uuid); + } + } + + saveStorage(storage); + refreshQuoteLists(); + + void resetRemovalQuotes(); +} + export function removeQuote(key: string, uuid: string): void { const storage = getStorage(); if (!storage.quotes.has(key)) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.js new file mode 100644 index 0000000000..110981720f --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes.js @@ -0,0 +1,24 @@ +/** + * Requests to reset the removal quotes. + * + * @author Olaf Braun + * @copyright 2001-2025 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.resetRemovalQuotes = resetRemovalQuotes; + async function resetRemovalQuotes() { + const url = new URL(window.WSC_RPC_API_URL + "core/messages/reset-removal-quotes"); + try { + await (0, Backend_1.prepareRequest)(url).post().fetchAsJson(); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)([]); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js index 5188e74aa2..c019d4e72b 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js @@ -59,9 +59,12 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui enableMobileMenu: true, pageMenuMainProvider: new Frontend_1.default(), }); + if (options.removeQuotes?.length) { + void new Promise((resolve_3, reject_3) => { require(["./Component/Quote/Storage"], resolve_3, reject_3); }).then(tslib_1.__importStar).then(({ removeQuotes }) => removeQuotes(options.removeQuotes)); + } UiPageHeaderMenu.init(); if (options.styleChanger) { - void new Promise((resolve_3, reject_3) => { require(["./Controller/Style/Changer"], resolve_3, reject_3); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => { + void new Promise((resolve_4, reject_4) => { require(["./Controller/Style/Changer"], resolve_4, reject_4); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => { ControllerStyleChanger.setup(); }); } @@ -96,22 +99,22 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui } } (0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => { - void new Promise((resolve_4, reject_4) => { require(["./Ui/Reaction/SummaryDetails"], resolve_4, reject_4); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_5, reject_5) => { require(["./Ui/Reaction/SummaryDetails"], resolve_5, reject_5); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment", () => { - void new Promise((resolve_5, reject_5) => { require(["./Component/Comment/woltlab-core-comment"], resolve_5, reject_5); }).then(tslib_1.__importStar); + void new Promise((resolve_6, reject_6) => { require(["./Component/Comment/woltlab-core-comment"], resolve_6, reject_6); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment-response", () => { - void new Promise((resolve_6, reject_6) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_6, reject_6); }).then(tslib_1.__importStar); + void new Promise((resolve_7, reject_7) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_7, reject_7); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-emoji-picker", () => { - void new Promise((resolve_7, reject_7) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_7, reject_7); }).then(tslib_1.__importStar); + void new Promise((resolve_8, reject_8) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_8, reject_8); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("[data-follow-user]", () => { - void new Promise((resolve_8, reject_8) => { require(["./Component/User/Follow"], resolve_8, reject_8); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_9, reject_9) => { require(["./Component/User/Follow"], resolve_9, reject_9); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("[data-ignore-user]", () => { - void new Promise((resolve_9, reject_9) => { require(["./Component/User/Ignore"], resolve_9, reject_9); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_10, reject_10) => { require(["./Component/User/Ignore"], resolve_10, reject_10); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); } }); 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 f086797376..bdbcf16a26 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -7,13 +7,14 @@ * @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"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_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"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.saveQuote = saveQuote; exports.saveFullQuote = saveFullQuote; exports.getQuotes = getQuotes; exports.getMessage = getMessage; + exports.removeQuotes = removeQuotes; exports.removeQuote = removeQuote; exports.markQuoteAsUsed = markQuoteAsUsed; exports.getUsedQuotes = getUsedQuotes; @@ -71,6 +72,17 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C const key = objectId ? getKey(objectType, objectId) : objectType; return getStorage().messages.get(key); } + function removeQuotes(uuids) { + const storage = getStorage(); + for (const uuid of uuids) { + for (const quotes of storage.quotes.values()) { + quotes.delete(uuid); + } + } + saveStorage(storage); + (0, List_1.refreshQuoteLists)(); + void (0, ResetRemovalQuotes_1.resetRemovalQuotes)(); + } function removeQuote(key, uuid) { const storage = getStorage(); if (!storage.quotes.has(key)) { diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/ResetRemovalQuotes.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/ResetRemovalQuotes.class.php new file mode 100644 index 0000000000..53bb44aebe --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/ResetRemovalQuotes.class.php @@ -0,0 +1,30 @@ + + * @since 6.2 + */ +#[PostRequest('/core/messages/reset-removal-quotes')] +final class ResetRemovalQuotes implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + MessageQuoteManager::getInstance()->reset(); + + return new JsonResponse([]); + } +} 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 500108d3ae..3f0a580a28 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -3,11 +3,8 @@ namespace wcf\system\message\quote; use wcf\data\IMessage; -use wcf\data\object\type\ObjectType; -use wcf\data\object\type\ObjectTypeCache; use wcf\system\event\EventHandler; use wcf\system\exception\SystemException; -use wcf\system\html\input\HtmlInputProcessor; use wcf\system\SingletonFactory; use wcf\system\WCF; use wcf\util\ArrayUtil; @@ -15,42 +12,12 @@ /** * Manages message quotes. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License */ class MessageQuoteManager extends SingletonFactory { - /** - * current object ids - * @var int[] - */ - protected $objectIDs = []; - - /** - * current object type name - * @var string - */ - protected $objectType = ''; - - /** - * list of object types - * @var ObjectType[] - */ - protected $objectTypes = []; - - /** - * list of stored quotes - * @var mixed[][] - */ - protected $quotes = []; - - /** - * list of quote messages by quote id - * @var array - */ - protected $quoteData = []; - /** * message id for quoting * @var int @@ -71,16 +38,8 @@ protected function init() // load stored quotes from session $messageQuotes = WCF::getSession()->getVar('__messageQuotes'); if (\is_array($messageQuotes)) { - $this->quotes = $messageQuotes['quotes'] ?? []; - $this->quoteData = $messageQuotes['quoteData'] ?? []; $this->removeQuoteIDs = $messageQuotes['removeQuoteIDs'] ?? []; } - - // load object types - $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.message.quote'); - foreach ($objectTypes as $objectType) { - $this->objectTypes[$objectType->objectType] = $objectType; - } } /** @@ -94,8 +53,10 @@ protected function init() * @param string $message * @param string $fullQuote * @param bool $returnFalseIfExists + * * @return mixed * @throws SystemException + * @deprecated 6.2 */ public function addQuote( $objectType, @@ -105,61 +66,7 @@ public function addQuote( $fullQuote = '', $returnFalseIfExists = true ) { - if (!isset($this->objectTypes[$objectType])) { - throw new SystemException("Object type '" . $objectType . "' is unknown"); - } - - if (!isset($this->quotes[$objectType])) { - $this->quotes[$objectType] = []; - } - - if (!isset($this->quotes[$objectType][$objectID])) { - $this->quotes[$objectType][$objectID] = []; - } - - $quoteID = $this->getQuoteID($objectType, $objectID, $message, $fullQuote); - if (!isset($this->quotes[$objectType][$objectID][$quoteID])) { - $this->quotes[$objectType][$objectID][$quoteID] = 0; - $this->quoteData[$quoteID] = $message; - - // save parent object id - - if (!isset($this->quoteData['parents'])) { - $this->quoteData['parents'] = []; - } - - if (!isset($this->quoteData['parents'][$objectType])) { - $this->quoteData['parents'][$objectType] = []; - } - - if (!isset($this->quoteData['parents'][$objectType][$parentObjectID])) { - $this->quoteData['parents'][$objectType][$parentObjectID] = []; - } - - $this->quoteData['parents'][$objectType][$parentObjectID][] = $objectID; - $this->quoteData[$quoteID . '_pID'] = $parentObjectID; - - if (!empty($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); - - $this->quotes[$objectType][$objectID][$quoteID] = 1; - $this->quoteData[$quoteID . '_fq'] = $htmlInputProcessor->getHtml(); - } - - $this->updateSession(); - } elseif ($returnFalseIfExists) { - return false; - } - - return $quoteID; + return false; } /** @@ -169,7 +76,9 @@ public function addQuote( * @param int $objectID * @param string $message * @param string $fullQuote + * * @return string + * @deprecated 6.2 */ public function getQuoteID($objectType, $objectID, $message, $fullQuote = '') { @@ -180,65 +89,12 @@ public function getQuoteID($objectType, $objectID, $message, $fullQuote = '') * Removes a quote from storage and returns true if the quote has successfully been removed. * * @param string $quoteID + * * @return bool + * @deprecated 6.2 */ public function removeQuote($quoteID) { - if (!isset($this->quoteData[$quoteID])) { - return false; - } - - foreach ($this->quotes as $objectType => $objectIDs) { - foreach ($objectIDs as $objectID => $quoteIDs) { - foreach ($quoteIDs as $qID => $isFullQuote) { - if ($qID == $quoteID) { - unset($this->quotes[$objectType][$objectID][$qID]); - - // clean-up structure - if (empty($this->quotes[$objectType][$objectID])) { - unset($this->quotes[$objectType][$objectID]); - - if (empty($this->quotes[$objectType])) { - unset($this->quotes[$objectType]); - } - } - - unset($this->quoteData[$quoteID]); - if ($isFullQuote) { - unset($this->quoteData[$quoteID . '_fq']); - } - - // remove parent object id reference - if (isset($this->quoteData[$quoteID . '_pID'])) { - $parentObjectID = $this->quoteData[$quoteID . '_pID']; - if (!isset($this->quotes[$objectType][$objectID])) { - if (isset($this->quoteData['parents'][$objectType][$parentObjectID][$objectID])) { - unset($this->quoteData['parents'][$objectType][$parentObjectID][$objectID]); - - // cleanup - if (empty($this->quoteData['parents'][$objectType][$parentObjectID])) { - unset($this->quoteData['parents'][$objectType][$parentObjectID]); - - if (empty($this->quoteData['parents'][$objectType])) { - unset($this->quoteData['parents'][$objectType]); - - if (empty($this->quoteData['parents'])) { - unset($this->quoteData['parents']); - } - } - } - } - } - } - - $this->updateSession(); - - return true; - } - } - } - } - return false; } @@ -246,34 +102,12 @@ public function removeQuote($quoteID) * Returns an array containing the quote author, link and text. * * @param string $quoteID + * * @return string[]|false + * @deprecated 6.2 */ public function getQuoteComponents($quoteID) { - if ($this->getQuote($quoteID, false) === null) { - return false; - } - - // find the quote and simulate a regular call to render quotes - foreach ($this->quotes as $objectType => $objectIDs) { - foreach ($objectIDs as $objectID => $quoteIDs) { - if (isset($quoteIDs[$quoteID])) { - $quoteHandler = \call_user_func([$this->objectTypes[$objectType]->className, 'getInstance']); - $renderedQuotes = $quoteHandler->renderQuotes([ - $objectID => [ - $quoteID => $quoteIDs[$quoteID], - ], - ], true, false); - - $this->markQuotesForRemoval([$quoteID]); - - $renderedQuotes[0]['isFullQuote'] = (isset($this->quoteData[$quoteID . '_fq'])); - - return $renderedQuotes[0]; - } - } - } - return false; } @@ -281,18 +115,14 @@ public function getQuoteComponents($quoteID) * Returns a list of quotes. * * @param bool $supportPaste + * * @return string + * + * @deprecated 6.2 */ public function getQuotes($supportPaste = false) { - $template = ''; - - foreach ($this->quotes as $objectType => $objectData) { - $quoteHandler = \call_user_func([$this->objectTypes[$objectType]->className, 'getInstance']); - $template .= $quoteHandler->render($objectData, $supportPaste); - } - - return $template; + return ''; } /** @@ -301,40 +131,14 @@ public function getQuotes($supportPaste = false) * @param string $objectType * @param int[] $objectIDs * @param bool $markForRemoval + * * @return string[] + * + * @deprecated 6.2 */ public function getQuotesByObjectIDs($objectType, array $objectIDs, $markForRemoval = true) { - if (!isset($this->quotes[$objectType])) { - return []; - } - - $data = []; - $removeQuoteIDs = []; - foreach ($this->quotes[$objectType] as $objectID => $quoteIDs) { - if (\in_array($objectID, $objectIDs)) { - $data[$objectID] = $quoteIDs; - - // mark quotes for removal - if ($markForRemoval) { - $removeQuoteIDs = \array_merge($removeQuoteIDs, \array_keys($quoteIDs)); - } - } - } - - // no quotes found - if (empty($data)) { - return []; - } - - // mark quotes for removal - if (!empty($removeQuoteIDs)) { - $this->markQuotesForRemoval($removeQuoteIDs); - } - - $quoteHandler = \call_user_func([$this->objectTypes[$objectType]->className, 'getInstance']); - - return $quoteHandler->renderQuotes($data); + return []; } /** @@ -343,40 +147,14 @@ public function getQuotesByObjectIDs($objectType, array $objectIDs, $markForRemo * @param string $objectType * @param int $parentObjectID * @param bool $markForRemoval + * * @return string[] + * + * @deprecated 6.2 */ public function getQuotesByParentObjectID($objectType, $parentObjectID, $markForRemoval = true) { - if (!isset($this->quoteData['parents'][$objectType][$parentObjectID])) { - return []; - } - - $data = []; - $removeQuoteIDs = []; - foreach ($this->quoteData['parents'][$objectType][$parentObjectID] as $objectID) { - if (isset($this->quotes[$objectType][$objectID])) { - $data[$objectID] = $this->quotes[$objectType][$objectID]; - - // mark quotes for removal - if ($markForRemoval) { - $removeQuoteIDs = \array_merge($removeQuoteIDs, \array_keys($data[$objectID])); - } - } - } - - // no quotes found - if (empty($data)) { - return []; - } - - // mark quotes for removal - if (!empty($removeQuoteIDs)) { - $this->markQuotesForRemoval($removeQuoteIDs); - } - - $quoteHandler = \call_user_func([$this->objectTypes[$objectType]->className, 'getInstance']); - - return $quoteHandler->renderQuotes($data, false); + return []; } /** @@ -384,16 +162,13 @@ public function getQuotesByParentObjectID($objectType, $parentObjectID, $markFor * * @param string $quoteID * @param bool $useFullQuote + * * @return string|null + * + * @deprecated 6.2 */ public function getQuote($quoteID, $useFullQuote = true) { - if ($useFullQuote && isset($this->quoteData[$quoteID . '_fq'])) { - return $this->quoteData[$quoteID . '_fq']; - } elseif (isset($this->quoteData[$quoteID])) { - return $this->quoteData[$quoteID]; - } - return null; } @@ -401,20 +176,13 @@ public function getQuote($quoteID, $useFullQuote = true) * Returns the object id by quote id. * * @param string $quoteID + * * @return int|null + * + * @deprecated 6.2 */ public function getObjectID($quoteID) { - if (isset($this->quoteData[$quoteID])) { - foreach ($this->quotes as $objectIDs) { - foreach ($objectIDs as $objectID => $quoteIDs) { - if (isset($quoteIDs[$quoteID])) { - return $objectID; - } - } - } - } - return null; } @@ -423,10 +191,10 @@ public function getObjectID($quoteID) * * @param string[] $quoteIDs */ - public function markQuotesForRemoval(array $quoteIDs) + public function markQuotesForRemoval(array $quoteIDs): void { foreach ($quoteIDs as $index => $quoteID) { - if (!isset($this->quoteData[$quoteID]) || \in_array($quoteID, $this->removeQuoteIDs)) { + if (\in_array($quoteID, $this->removeQuoteIDs)) { unset($quoteIDs[$index]); } } @@ -443,7 +211,9 @@ public function markQuotesForRemoval(array $quoteIDs) * @param IMessage $message * @param string $text * @param bool $renderAsString + * * @return array|string + * @deprecated 6.2 */ public function renderQuote(IMessage $message, $text, $renderAsString = true) { @@ -469,68 +239,36 @@ public function renderQuote(IMessage $message, $text, $renderAsString = true) /** * Removes quotes marked for removal. + * + * @deprecated 6.2 */ public function removeMarkedQuotes() { - if (!empty($this->removeQuoteIDs)) { - foreach ($this->removeQuoteIDs as $quoteID) { - $this->removeQuote($quoteID); - } - - // reset list of quote ids marked for removal - $this->removeQuoteIDs = []; - - $this->updateSession(); - } } /** * Returns the number of stored quotes. * * @return int + * @deprecated 6.2 */ public function countQuotes() { - $count = 0; - foreach ($this->quoteData as $quoteID => $quote) { - if (\strlen($quoteID) == 8) { - $count++; - } - } - - return $count; + return 0; } /** * Returns a list of full quotes by object id for given object types. * * @param string[] $objectTypes + * * @return mixed[][] * @throws SystemException + * @deprecated 6.2 */ public function getFullQuoteObjectIDs(array $objectTypes) { - $objectIDs = []; - - foreach ($objectTypes as $objectType) { - if (!isset($this->objectTypes[$objectType])) { - throw new SystemException("Object type '" . $objectType . "' is unknown"); - } - - $objectIDs[$objectType] = []; - if (isset($this->quotes[$objectType])) { - foreach ($this->quotes[$objectType] as $objectID => $quotes) { - foreach ($quotes as $isFullQuote) { - if ($isFullQuote) { - $objectIDs[$objectType][] = $objectID; - break; - } - } - } - } - } - - return $objectIDs; + return []; } /** @@ -538,16 +276,13 @@ public function getFullQuoteObjectIDs(array $objectTypes) * * @param string $objectType * @param int[] $objectIDs + * * @throws SystemException + * + * @deprecated 6.2 */ public function initObjects($objectType, array $objectIDs) { - if (!isset($this->objectTypes[$objectType])) { - throw new SystemException("Object type '" . $objectType . "' is unknown"); - } - - $this->objectIDs = ArrayUtil::toIntegerArray($objectIDs); - $this->objectType = $objectType; } /** @@ -567,11 +302,6 @@ public function readFormParameters() { if (isset($_REQUEST['__removeQuoteIDs']) && \is_array($_REQUEST['__removeQuoteIDs'])) { $quoteIDs = ArrayUtil::trim($_REQUEST['__removeQuoteIDs']); - foreach ($quoteIDs as $index => $quoteID) { - if (!isset($this->quoteData[$quoteID])) { - unset($quoteIDs[$index]); - } - } if (!empty($quoteIDs)) { $this->removeQuoteIDs = \array_merge($this->removeQuoteIDs, $quoteIDs); @@ -584,35 +314,16 @@ public function readFormParameters() */ public function saved() { - $this->removeMarkedQuotes(); + $this->updateSession(); } /** * Assigns variables on page load. + * + * @deprecated 6.2 */ public function assignVariables() { - $fullQuoteObjectIDs = []; - if (!empty($this->objectType) && !empty($this->objectIDs) && isset($this->quotes[$this->objectType])) { - foreach ($this->quotes[$this->objectType] as $objectID => $quotes) { - if (!\in_array($objectID, $this->objectIDs)) { - continue; - } - - foreach ($quotes as $isFullQuote) { - if ($isFullQuote) { - $fullQuoteObjectIDs[] = $objectID; - break; - } - } - } - } - - WCF::getTPL()->assign([ - '__quoteCount' => $this->countQuotes(), - '__quoteFullQuote' => $fullQuoteObjectIDs, - '__quoteRemove' => $this->removeQuoteIDs, - ]); } /** @@ -629,35 +340,55 @@ public function getQuoteMessageID() * Removes orphaned quote ids * * @param int[] $quoteIDs + * + * @deprecated 6.2 */ public function removeOrphanedQuotes(array $quoteIDs) { - foreach ($quoteIDs as $quoteID) { - $this->removeQuote($quoteID); - } - - $this->updateSession(); } /** * Returns true if a quote id represents a full quote. * * @param string $quoteID + * * @return bool + * @deprecated 6.2 */ public function isFullQuote($quoteID) { - return isset($this->quoteData[$quoteID . '_fq']); + return false; + } + + /** + * Returns the list of quote uuids to be removed. + * + * @return string[] + * @since 6.2 + */ + public function getRemoveQuoteIDs(): array + { + return $this->removeQuoteIDs; + } + + /** + * Resets the list of quote uuids to be removed. + * + * @since 6.2 + */ + public function reset(): void + { + $this->removeQuoteIDs = []; + + $this->updateSession(); } /** * Updates data stored in session, */ - protected function updateSession() + protected function updateSession(): void { WCF::getSession()->register('__messageQuotes', [ - 'quotes' => $this->quotes, - 'quoteData' => $this->quoteData, 'removeQuoteIDs' => $this->removeQuoteIDs, ]); } From a3f2fb0c1fba2d2d36e5375a5926b85cfca3ddda Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:42:55 +0100 Subject: [PATCH 31/65] Mark `QuotedMessage` as deprecated --- .../files/lib/system/message/quote/QuotedMessage.class.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php b/wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php index 709ea1accb..db856fa470 100644 --- a/wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/QuotedMessage.class.php @@ -21,6 +21,8 @@ * @method int getUserID() * @method string getUsername() * @method bool isVisible() + * + * @deprecated 6.2 */ class QuotedMessage implements \Countable, \Iterator { From 58b8df02bcb9065e0c69b902a27e501624610582 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:43:46 +0100 Subject: [PATCH 32/65] Remove the deprecated marker from `MessageQuoteManager::renderQuote()` --- .../files/lib/system/message/quote/MessageQuoteManager.class.php | 1 - 1 file changed, 1 deletion(-) 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 3f0a580a28..95a42b4216 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -213,7 +213,6 @@ public function markQuotesForRemoval(array $quoteIDs): void * @param bool $renderAsString * * @return array|string - * @deprecated 6.2 */ public function renderQuote(IMessage $message, $text, $renderAsString = true) { From 715f9e1234a966e85e0d2d57e2180d61489dcb41 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:54:16 +0100 Subject: [PATCH 33/65] Don't use `MessageQuoteManager::assignVariables()` --- .../form/builder/field/wysiwyg/WysiwygFormField.class.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php index c602290184..e8deb29080 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php @@ -136,10 +136,6 @@ public function getAutosaveId() */ public function getFieldHtml() { - if ($this->supportsQuotes()) { - MessageQuoteManager::getInstance()->assignVariables(); - } - /** @noinspection PhpUndefinedFieldInspection */ $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission; if ($disallowedBBCodesPermission === null) { From bc802c1dc851c8f6546be3b0e711e9aeef5971f6 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:54:45 +0100 Subject: [PATCH 34/65] Register endpoint to delete removal quote uudis --- wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php | 1 + 1 file changed, 1 insertion(+) diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 8110ba1e3e..70c9fb9523 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -139,6 +139,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $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()); $event->register(new \wcf\system\endpoint\controller\core\moderationQueues\ChangeJustifiedStatus()); From ce28e250d39daab99ec6b84051f9010ab159b86e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:55:11 +0100 Subject: [PATCH 35/65] Mark `IMessageQuoteAction` as deprecated --- wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php b/wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php index 2ba2fed7bd..c711053b29 100644 --- a/wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php +++ b/wcfsetup/install/files/lib/data/IMessageQuoteAction.class.php @@ -8,6 +8,8 @@ * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 */ interface IMessageQuoteAction { From 62c9aff288e37171c52e0a10708cf372bdafaf63 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 10:55:32 +0100 Subject: [PATCH 36/65] Change comment for the saved function --- .../lib/system/message/quote/MessageQuoteManager.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 95a42b4216..52593ee92f 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -309,9 +309,9 @@ public function readFormParameters() } /** - * Removes quotes after saving current message. + * Store the quote uuids that should be removed in the next request. */ - public function saved() + public function saved(): void { $this->updateSession(); } From 03ec3fdc83337253c25e7a851b685ef4581828a7 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 11:02:48 +0100 Subject: [PATCH 37/65] Remove the `active` status when the quote for an editor is deleted --- .../Core/Component/Quote/Storage.ts | 20 ++++++++++++++++++- .../Core/Component/Quote/Storage.js | 18 +++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 806b45601a..61297d265a 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -13,6 +13,7 @@ import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; import { messageAuthor } 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"; interface Message { objectID: number; @@ -116,6 +117,13 @@ export function removeQuotes(uuids: string[]): void { } } + for (const [key, quotes] of storage.quotes) { + if (quotes.size === 0) { + storage.quotes.delete(key); + storage.messages.delete(key); + } + } + saveStorage(storage); refreshQuoteLists(); @@ -154,9 +162,15 @@ export function getUsedQuotes(editorId: string): Set { export function clearQuotesForEditor(editorId: string): void { const storage = getStorage(); + const fullQuotes: string[] = []; usedQuotes.get(editorId)?.forEach((uuid) => { - for (const quotes of storage.quotes.values()) { + for (const [key, quotes] of storage.quotes) { + const quote = quotes.get(uuid); + if (quote?.rawMessage !== undefined) { + fullQuotes.push(key); + } + quotes.delete(uuid); } }); @@ -172,6 +186,10 @@ export function clearQuotesForEditor(editorId: string): void { saveStorage(storage); refreshQuoteLists(); + + fullQuotes.forEach((key) => { + removeQuoteStatus(key); + }); } export function isFullQuoted(objectType: string, objectId: number): boolean { 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 bdbcf16a26..d325fcaa69 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"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_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"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_1, Message_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.saveQuote = saveQuote; @@ -79,6 +79,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C quotes.delete(uuid); } } + for (const [key, quotes] of storage.quotes) { + if (quotes.size === 0) { + storage.quotes.delete(key); + storage.messages.delete(key); + } + } saveStorage(storage); (0, List_1.refreshQuoteLists)(); void (0, ResetRemovalQuotes_1.resetRemovalQuotes)(); @@ -107,8 +113,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C } function clearQuotesForEditor(editorId) { const storage = getStorage(); + const fullQuotes = []; usedQuotes.get(editorId)?.forEach((uuid) => { - for (const quotes of storage.quotes.values()) { + for (const [key, quotes] of storage.quotes) { + const quote = quotes.get(uuid); + if (quote?.rawMessage !== undefined) { + fullQuotes.push(key); + } quotes.delete(uuid); } }); @@ -121,6 +132,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C } saveStorage(storage); (0, List_1.refreshQuoteLists)(); + fullQuotes.forEach((key) => { + (0, Message_1.removeQuoteStatus)(key); + }); } function isFullQuoted(objectType, objectId) { const key = getKey(objectType, objectId); From 48453d9a66ea883b3686f8603c39dcdf2ec15c61 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 11:29:46 +0100 Subject: [PATCH 38/65] Go to the page `href` if there is no active editor and `href` is a valid link --- ts/WoltLabSuite/Core/Component/Quote/Message.ts | 13 ++++++++++--- .../js/WoltLabSuite/Core/Component/Quote/Message.js | 12 +++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 414e675083..b7f47904c1 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -72,7 +72,7 @@ export function registerContainer( container.classList.add("jsQuoteMessageContainer"); const quoteMessage = container.querySelector(".jsQuoteMessage"); - const quoteMessageButton = quoteMessage?.querySelector(".button"); + const quoteMessageButton = quoteMessage?.querySelector(".button"); if (quoteMessageButton) { quoteMessageButtons.set(getKey(objectType, objectId), quoteMessageButton); @@ -87,6 +87,7 @@ export function registerContainer( event.preventDefault(); const quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!); + quoteMessageButton!.classList.add("active"); if (activeEditor !== undefined) { dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ @@ -97,9 +98,15 @@ export function registerContainer( }); markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); + } else { + // Check if the href is a valid URL and navigate to it. + try { + const url = new URL(quoteMessageButton!.getAttribute("href")!); + window.location.href = url.href; + } catch { + // Ignore any errors + } } - - quoteMessageButton!.classList.add("active"); }), ); }); 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 83517b1330..4588d3929f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -49,6 +49,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui quoteMessage?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { event.preventDefault(); const 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({ author: quoteMessage.author, @@ -58,7 +59,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui }); (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); } - quoteMessageButton.classList.add("active"); + else { + // Check if the href is a valid URL and navigate to it. + try { + const url = new URL(quoteMessageButton.getAttribute("href")); + window.location.href = url.href; + } + catch { + // Ignore any errors + } + } })); }); } From 6966f58e108a887a93bc024265f63cab49e222e8 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 11:37:04 +0100 Subject: [PATCH 39/65] Reset the active editor as soon as the form has been successfully submitted --- ts/WoltLabSuite/Core/Component/Comment/Add.ts | 4 ++++ ts/WoltLabSuite/Core/Component/Comment/Response/Add.ts | 4 ++++ ts/WoltLabSuite/Core/Ui/Message/Reply.ts | 2 ++ .../files/js/WoltLabSuite/Core/Component/Comment/Add.js | 4 +++- .../js/WoltLabSuite/Core/Component/Comment/Response/Add.js | 4 +++- .../install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js | 3 ++- 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Comment/Add.ts b/ts/WoltLabSuite/Core/Component/Comment/Add.ts index 988d39f077..e74ec184e8 100644 --- a/ts/WoltLabSuite/Core/Component/Comment/Add.ts +++ b/ts/WoltLabSuite/Core/Component/Comment/Add.ts @@ -17,6 +17,8 @@ import { listenToCkeditor } from "../Ckeditor/Event"; import { createComment } from "WoltLabSuite/Core/Api/Comments/CreateComment"; import { getGuestToken } from "../GuestTokenDialog"; import User from "WoltLabSuite/Core/User"; +import { clearQuotesForEditor } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; type CallbackInsertComment = (commentId: number) => void; @@ -180,6 +182,8 @@ export class CommentAdd { */ #reset(): void { this.#getEditor().reset(); + clearQuotesForEditor(this.#getEditor().sourceElement.id); + setActiveEditor(); if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); diff --git a/ts/WoltLabSuite/Core/Component/Comment/Response/Add.ts b/ts/WoltLabSuite/Core/Component/Comment/Response/Add.ts index 3d80c75ca1..b7d7dfe438 100644 --- a/ts/WoltLabSuite/Core/Component/Comment/Response/Add.ts +++ b/ts/WoltLabSuite/Core/Component/Comment/Response/Add.ts @@ -17,6 +17,8 @@ import { listenToCkeditor } from "../../Ckeditor/Event"; import User from "WoltLabSuite/Core/User"; import { getGuestToken } from "../../GuestTokenDialog"; import { createResponse } from "WoltLabSuite/Core/Api/Comments/Responses/CreateResponse"; +import { clearQuotesForEditor } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; type CallbackInsertResponse = (commentId: number, responseId: number) => void; @@ -131,6 +133,8 @@ export class CommentResponseAdd { */ #reset(): void { this.#getEditor().reset(); + clearQuotesForEditor(this.#getEditor().sourceElement.id); + setActiveEditor(); if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); diff --git a/ts/WoltLabSuite/Core/Ui/Message/Reply.ts b/ts/WoltLabSuite/Core/Ui/Message/Reply.ts index ba3cf6bee6..8571592a6f 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Reply.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Reply.ts @@ -23,6 +23,7 @@ import * as UiScroll from "../Scroll"; import { CKEditor, getCkeditor } from "../../Component/Ckeditor"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; import { clearQuotesForEditor } from "WoltLabSuite/Core/Component/Quote/Storage"; +import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message"; interface MessageReplyOptions { ajax: { @@ -388,6 +389,7 @@ class UiMessageReply { this._guestDialogId = guestDialogId; } else { clearQuotesForEditor(this._textarea.id); + setActiveEditor(); this._insertMessage(data); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Add.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Add.js index c27c8d2fa9..8622aa0b13 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Add.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Add.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @since 6.0 */ -define(["require", "exports", "tslib", "../../Ui/Scroll", "../../Ui/Notification", "../../Language", "../../Event/Handler", "../../Dom/Util", "../Ckeditor", "../Ckeditor/Event", "WoltLabSuite/Core/Api/Comments/CreateComment", "../GuestTokenDialog", "WoltLabSuite/Core/User"], function (require, exports, tslib_1, UiScroll, UiNotification, Language_1, EventHandler, Util_1, Ckeditor_1, Event_1, CreateComment_1, GuestTokenDialog_1, User_1) { +define(["require", "exports", "tslib", "../../Ui/Scroll", "../../Ui/Notification", "../../Language", "../../Event/Handler", "../../Dom/Util", "../Ckeditor", "../Ckeditor/Event", "WoltLabSuite/Core/Api/Comments/CreateComment", "../GuestTokenDialog", "WoltLabSuite/Core/User", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, UiScroll, UiNotification, Language_1, EventHandler, Util_1, Ckeditor_1, Event_1, CreateComment_1, GuestTokenDialog_1, User_1, Storage_1, Message_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommentAdd = void 0; @@ -149,6 +149,8 @@ define(["require", "exports", "tslib", "../../Ui/Scroll", "../../Ui/Notification */ #reset() { this.#getEditor().reset(); + (0, Storage_1.clearQuotesForEditor)(this.#getEditor().sourceElement.id); + (0, Message_1.setActiveEditor)(); if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Response/Add.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Response/Add.js index d22c92530f..3d483b720d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Response/Add.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Comment/Response/Add.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @since 6.0 */ -define(["require", "exports", "tslib", "../../../Dom/Util", "../../../Language", "../../../Event/Handler", "../../../Ui/Scroll", "../../../Ui/Notification", "../../Ckeditor", "../../Ckeditor/Event", "WoltLabSuite/Core/User", "../../GuestTokenDialog", "WoltLabSuite/Core/Api/Comments/Responses/CreateResponse"], function (require, exports, tslib_1, Util_1, Language_1, EventHandler, UiScroll, UiNotification, Ckeditor_1, Event_1, User_1, GuestTokenDialog_1, CreateResponse_1) { +define(["require", "exports", "tslib", "../../../Dom/Util", "../../../Language", "../../../Event/Handler", "../../../Ui/Scroll", "../../../Ui/Notification", "../../Ckeditor", "../../Ckeditor/Event", "WoltLabSuite/Core/User", "../../GuestTokenDialog", "WoltLabSuite/Core/Api/Comments/Responses/CreateResponse", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Util_1, Language_1, EventHandler, UiScroll, UiNotification, Ckeditor_1, Event_1, User_1, GuestTokenDialog_1, CreateResponse_1, Storage_1, Message_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommentResponseAdd = void 0; @@ -106,6 +106,8 @@ define(["require", "exports", "tslib", "../../../Dom/Util", "../../../Language", */ #reset() { this.#getEditor().reset(); + (0, Storage_1.clearQuotesForEditor)(this.#getEditor().sourceElement.id); + (0, Message_1.setActiveEditor)(); if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js index 67712bfb04..3a23f90b8a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util", "../Dialog", "../Notification", "../../User", "../../Controller/Captcha", "../Scroll", "../../Component/Ckeditor", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Quote/Storage"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1, Dialog_1, UiNotification, User_1, Captcha_1, UiScroll, Ckeditor_1, Event_1, Storage_1) { +define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util", "../Dialog", "../Notification", "../../User", "../../Controller/Captcha", "../Scroll", "../../Component/Ckeditor", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1, Dialog_1, UiNotification, User_1, Captcha_1, UiScroll, Ckeditor_1, Event_1, Storage_1, Message_1) { "use strict"; Ajax = tslib_1.__importStar(Ajax); Core = tslib_1.__importStar(Core); @@ -307,6 +307,7 @@ define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/ } else { (0, Storage_1.clearQuotesForEditor)(this._textarea.id); + (0, Message_1.setActiveEditor)(); this._insertMessage(data); if (!User_1.default.userId) { Dialog_1.default.close(data.returnValues.guestDialogID); From ccfa0ea964a0493a3de6a3ead43e3dbbac20b5ab Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 12:42:45 +0100 Subject: [PATCH 40/65] If the editor is no longer visible, do not set an editor as active for quoting --- ts/WoltLabSuite/Core/Ui/Message/Reply.ts | 4 +++- .../install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ts/WoltLabSuite/Core/Ui/Message/Reply.ts b/ts/WoltLabSuite/Core/Ui/Message/Reply.ts index 8571592a6f..27f9228c3a 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Reply.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Reply.ts @@ -389,7 +389,9 @@ class UiMessageReply { this._guestDialogId = guestDialogId; } else { clearQuotesForEditor(this._textarea.id); - setActiveEditor(); + if (!this._getCKEditor().isVisible()) { + setActiveEditor(); + } this._insertMessage(data); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js index 3a23f90b8a..8f7e2c77e1 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js @@ -307,7 +307,9 @@ define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/ } else { (0, Storage_1.clearQuotesForEditor)(this._textarea.id); - (0, Message_1.setActiveEditor)(); + if (!this._getCKEditor().isVisible()) { + (0, Message_1.setActiveEditor)(); + } this._insertMessage(data); if (!User_1.default.userId) { Dialog_1.default.close(data.returnValues.guestDialogID); From 861904750eb5bbc9c52bba7cf472675b60b24a21 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 12:43:24 +0100 Subject: [PATCH 41/65] =?UTF-8?q?Add=20comment=20why=20`.getAttribute(?= =?UTF-8?q?=E2=80=98href=E2=80=99)`=20is=20used=20instead=20of=20`.href`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ts/WoltLabSuite/Core/Component/Quote/Message.ts | 1 + .../files/js/WoltLabSuite/Core/Component/Quote/Message.js | 1 + 2 files changed, 2 insertions(+) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index b7f47904c1..bcc92adf89 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -100,6 +100,7 @@ export function registerContainer( markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); } else { // Check if the href is a valid URL and navigate to it. + // Don't use the `quoteMessageButton.href` property directly, as it might be a relative URL. try { const url = new URL(quoteMessageButton!.getAttribute("href")!); window.location.href = url.href; 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 4588d3929f..17e4b0f4a3 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -61,6 +61,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui } else { // Check if the href is a valid URL and navigate to it. + // Don't use the `quoteMessageButton.href` property directly, as it might be a relative URL. try { const url = new URL(quoteMessageButton.getAttribute("href")); window.location.href = url.href; From da69aa332ecc6684beabee2cd18ef93d5e02329e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 15:44:22 +0100 Subject: [PATCH 42/65] No redirect --- ts/WoltLabSuite/Core/Component/Quote/Message.ts | 9 --------- .../js/WoltLabSuite/Core/Component/Quote/Message.js | 11 ----------- 2 files changed, 20 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index bcc92adf89..7ca07fcc82 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -98,15 +98,6 @@ export function registerContainer( }); markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); - } else { - // Check if the href is a valid URL and navigate to it. - // Don't use the `quoteMessageButton.href` property directly, as it might be a relative URL. - try { - const url = new URL(quoteMessageButton!.getAttribute("href")!); - window.location.href = url.href; - } catch { - // Ignore any errors - } } }), ); 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 17e4b0f4a3..e6c299521c 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -59,17 +59,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui }); (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); } - else { - // Check if the href is a valid URL and navigate to it. - // Don't use the `quoteMessageButton.href` property directly, as it might be a relative URL. - try { - const url = new URL(quoteMessageButton.getAttribute("href")); - window.location.href = url.href; - } - catch { - // Ignore any errors - } - } })); }); } From 3046a7e51e84d6459989a41b9c6520ce45073086 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 14 Jan 2025 15:47:17 +0100 Subject: [PATCH 43/65] Mark `renderQuote()`, `readParameters()` and `getQuoteMessageID()` as deprecated --- .../quote/MessageQuoteManager.class.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) 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 52593ee92f..726522dfbc 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -18,17 +18,11 @@ */ class MessageQuoteManager extends SingletonFactory { - /** - * message id for quoting - * @var int - */ - protected $quoteMessageID = 0; - /** * list of quote ids to be removed * @var string[] */ - protected $removeQuoteIDs = []; + protected array $removeQuoteIDs = []; /** * @inheritDoc @@ -213,6 +207,7 @@ public function markQuotesForRemoval(array $quoteIDs): void * @param bool $renderAsString * * @return array|string + * @deprecated 6.2 */ public function renderQuote(IMessage $message, $text, $renderAsString = true) { @@ -286,18 +281,17 @@ public function initObjects($objectType, array $objectIDs) /** * Reads the quote message id. + * + * @deprecated 6.2 */ public function readParameters() { - if (isset($_REQUEST['quoteMessageID'])) { - $this->quoteMessageID = \intval($_REQUEST['quoteMessageID']); - } } /** * Reads a list of quote ids to remove. */ - public function readFormParameters() + public function readFormParameters(): void { if (isset($_REQUEST['__removeQuoteIDs']) && \is_array($_REQUEST['__removeQuoteIDs'])) { $quoteIDs = ArrayUtil::trim($_REQUEST['__removeQuoteIDs']); @@ -329,10 +323,11 @@ public function assignVariables() * Returns quote message id. * * @return int + * @deprecated 6.2 */ public function getQuoteMessageID() { - return $this->quoteMessageID; + return 0; } /** From 4169c857e60ec2f3a14c8fef6ed001245d9d79a5 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 15 Jan 2025 08:57:05 +0100 Subject: [PATCH 44/65] Deprecated `WysiwygFormField::quoteData()` and added the replacement `WysiwygFormField::quoteSetting()` --- .../wysiwyg/WysiwygFormContainer.class.php | 31 ++++++++++++-- .../field/wysiwyg/WysiwygFormField.class.php | 40 ++++++++++++++----- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php index 50c3d755ea..c4f4ba1ba5 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php @@ -480,9 +480,9 @@ public function populate() ->supportMentions($this->supportMentions) ->supportQuotes($this->supportQuotes); if ($this->quoteData !== null) { - $this->wysiwygField->quoteData( + $this->wysiwygField->quoteSetting( $this->quoteData['objectType'], - $this->quoteData['actionClass'], + $this->quoteData['objectClass'], $this->quoteData['selectors'] ); } @@ -567,18 +567,41 @@ public function preselect($preselect = 'true') * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type * @param string $actionClass action class implementing `wcf\data\IMessageQuoteAction` * @param string[] $selectors selectors for the quotable content (required keys: `container`, `messageBody`, and `messageContent`) + * * @return static + * + * @deprecated 6.2 use `quoteSetting()` instead */ public function quoteData($objectType, $actionClass, array $selectors = []) + { + // Remove the `Action` suffix from the action class + $objectClass = \substr($actionClass, 0, -6); + + return $this->quoteSetting($objectType, $objectClass, $selectors); + } + + /** + * Sets the data required for advanced quote support for when quotable content is present + * on the active page and returns this container. + * + * Calling this method automatically enables quote support for this container. + * + * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type + * @param string $objectClass message object class implementing `wcf\data\IMessage` + * @param string[] $selectors selectors for the quotable content (required keys: `container`and `messageBody`) + * + * @return static + */ + public function quoteSetting(string $objectType, string $objectClass, array $selectors = []): static { if ($this->wysiwygField !== null) { - $this->wysiwygField->quoteData($objectType, $actionClass, $selectors); + $this->wysiwygField->quoteSetting($objectType, $objectClass, $selectors); } else { $this->supportQuotes(); // the parameters are validated by `WysiwygFormField` $this->quoteData = [ - 'actionClass' => $actionClass, + 'actionClass' => $objectClass, 'objectType' => $objectType, 'selectors' => $selectors, ]; diff --git a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php index e8deb29080..7a4ec1fed5 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php @@ -2,7 +2,7 @@ namespace wcf\system\form\builder\field\wysiwyg; -use wcf\data\IMessageQuoteAction; +use wcf\data\IMessage; use wcf\data\object\type\ObjectTypeCache; use wcf\system\bbcode\BBCodeHandler; use wcf\system\form\builder\data\processor\CustomFormDataProcessor; @@ -246,11 +246,35 @@ function (IFormDocument $document, array $parameters) { * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type * @param string $actionClass action class implementing `wcf\data\IMessageQuoteAction` * @param string[] $selectors selectors for the quotable content (required keys: `container`, `messageBody`, and `messageContent`) + * * @return static * * @throws \InvalidArgumentException if any of the given arguments is invalid + * + * @deprecated 6.2 use `quoteSetting()` instead */ public function quoteData($objectType, $actionClass, array $selectors = []) + { + // Remove the `Action` suffix from the action class + $objectClass = \substr($actionClass, 0, -6); + + return $this->quoteSetting($objectType, $objectClass, $selectors); + } + + /** + * Sets the data required for advanced quote support for when quotable content is present + * on the active page and returns this field. + * + * Calling this method automatically enables quote support for this field. + * + * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type + * @param string $objectClass message object class implementing `wcf\data\IMessage` + * @param string[] $selectors selectors for the quotable content (required keys: `container` and `messageBody``) + * + * @return static + * @since 6.2 + */ + public function quoteSetting(string $objectType, string $objectClass, array $selectors = []): static { if ( ObjectTypeCache::getInstance()->getObjectTypeByName( @@ -263,17 +287,17 @@ public function quoteData($objectType, $actionClass, array $selectors = []) ); } - if (!\class_exists($actionClass)) { - throw new \InvalidArgumentException("Unknown class '{$actionClass}' for field '{$this->getId()}'."); + if (!\class_exists($objectClass)) { + throw new \InvalidArgumentException("Unknown class '{$objectClass}' for field '{$this->getId()}'."); } - if (!\is_subclass_of($actionClass, IMessageQuoteAction::class)) { + if (!\is_subclass_of($objectClass, IMessage::class)) { throw new \InvalidArgumentException( - "'{$actionClass}' does not implement '" . IMessageQuoteAction::class . "' for field '{$this->getId()}'." + "'{$objectClass}' does not implement '" . IMessage::class . "' for field '{$this->getId()}'." ); } if (!empty($selectors)) { - foreach (['container', 'messageBody', 'messageContent'] as $selector) { + foreach (['container', 'messageBody'] as $selector) { if (!isset($selectors[$selector])) { throw new \InvalidArgumentException("Missing selector '{$selector}' for field '{$this->getId()}'."); } @@ -283,7 +307,7 @@ public function quoteData($objectType, $actionClass, array $selectors = []) $this->supportQuotes(); $this->quoteData = [ - 'actionClass' => $actionClass, + 'objectClass' => $objectClass, 'objectType' => $objectType, 'selectors' => $selectors, ]; @@ -350,8 +374,6 @@ public function supportQuotes($supportQuotes = true) if (!$this->supportsQuotes()) { // unset previously set quote data $this->quoteData = null; - } else { - MessageQuoteManager::getInstance()->readParameters(); } return $this; From a06a5a77a6eab6d7a15461835090c4d20bb27df5 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 15 Jan 2025 09:27:27 +0100 Subject: [PATCH 45/65] Implement a new `WysiwygTabFormContainer`, which can have an icon --- .../shared_wysiwygTabMenuFormContainer.tpl | 30 +++++++++++++++- .../Dependency/Container/WysiwygTabMenu.ts | 36 +++++++++++++++++++ .../Dependency/Container/WysiwygTabMenu.js | 35 ++++++++++++++++++ .../wysiwyg/WysiwygFormContainer.class.php | 10 +++--- .../WysiwygSmileyFormContainer.class.php | 8 +++++ .../wysiwyg/WysiwygTabFormContainer.class.php | 36 +++++++++++++++++++ 6 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.js create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php diff --git a/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl b/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl index 62d4b60d46..d477f05618 100644 --- a/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl +++ b/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl @@ -1 +1,29 @@ -{include file='shared_tabMenuFormContainer' __tabMenuCSSClassName='messageTabMenuNavigation'} +
    getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}{if !$container->checkDependencies()} hidden{/if}> + + + {include file='shared_formContainerChildren'} +
    + +{include file='shared_formContainerDependencies'} + + diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.ts new file mode 100644 index 0000000000..ed14b8d883 --- /dev/null +++ b/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.ts @@ -0,0 +1,36 @@ +/** + * Container visibility handler implementation for a wysiwyg tab menu that checks visibility + * based on the visibility of its tab menu list items. + * + * @author Olaf BRaun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ + +import Abstract from "./Abstract"; +import * as DependencyManager from "../Manager"; +import * as DomUtil from "../../../../../Dom/Util"; + +export class WysiwygTabMenu extends Abstract { + public checkContainer(): void { + // only consider containers that have not been hidden by their own dependencies + if (DependencyManager.isHiddenByDependencies(this._container)) { + return; + } + + const containerIsVisible = !this._container.hidden; + const listItems = this._container.parentNode!.querySelectorAll( + "#" + DomUtil.identify(this._container) + " > nav > ul > li", + ); + const containerShouldBeVisible = Array.from(listItems).some((child: HTMLElement) => !child.hidden); + + if (containerIsVisible !== containerShouldBeVisible) { + this._container.hidden = !containerShouldBeVisible; + + // check containers again to make sure parent containers can react to + // changing the visibility of this container + DependencyManager.checkContainers(); + } + } +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.js new file mode 100644 index 0000000000..5d1766b3f3 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTabMenu.js @@ -0,0 +1,35 @@ +/** + * Container visibility handler implementation for a wysiwyg tab menu that checks visibility + * based on the visibility of its tab menu list items. + * + * @author Olaf BRaun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ +define(["require", "exports", "tslib", "./Abstract", "../Manager", "../../../../../Dom/Util"], function (require, exports, tslib_1, Abstract_1, DependencyManager, DomUtil) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.WysiwygTabMenu = void 0; + Abstract_1 = tslib_1.__importDefault(Abstract_1); + DependencyManager = tslib_1.__importStar(DependencyManager); + DomUtil = tslib_1.__importStar(DomUtil); + class WysiwygTabMenu extends Abstract_1.default { + checkContainer() { + // only consider containers that have not been hidden by their own dependencies + if (DependencyManager.isHiddenByDependencies(this._container)) { + return; + } + const containerIsVisible = !this._container.hidden; + const listItems = this._container.parentNode.querySelectorAll("#" + DomUtil.identify(this._container) + " > nav > ul > li"); + const containerShouldBeVisible = Array.from(listItems).some((child) => !child.hidden); + if (containerIsVisible !== containerShouldBeVisible) { + this._container.hidden = !containerShouldBeVisible; + // check containers again to make sure parent containers can react to + // changing the visibility of this container + DependencyManager.checkContainers(); + } + } + } + exports.WysiwygTabMenu = WysiwygTabMenu; +}); diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php index 50c3d755ea..c585f2aee5 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php @@ -8,7 +8,6 @@ use wcf\system\event\EventHandler; use wcf\system\form\builder\button\wysiwyg\WysiwygPreviewFormButton; use wcf\system\form\builder\container\FormContainer; -use wcf\system\form\builder\container\TabFormContainer; use wcf\system\form\builder\field\TMaximumLengthFormField; use wcf\system\form\builder\field\TMinimumLengthFormField; use wcf\system\form\builder\field\wysiwyg\WysiwygAttachmentFormField; @@ -509,20 +508,23 @@ public function populate() ->appendChildren([ $this->smiliesContainer, - TabFormContainer::create($this->wysiwygId . 'AttachmentsTab') + WysiwygTabFormContainer::create($this->wysiwygId . 'AttachmentsTab') ->addClass('formAttachmentContent') ->label('wcf.attachment.attachments') + ->setIcon('paperclip') ->appendChild( FormContainer::create($this->wysiwygId . 'AttachmentsContainer') ->appendChild($this->attachmentField) ), - TabFormContainer::create($this->wysiwygId . 'SettingsTab') + WysiwygTabFormContainer::create($this->wysiwygId . 'SettingsTab') ->label('wcf.message.settings') + ->setIcon('gear') ->appendChild($this->settingsContainer), - TabFormContainer::create($this->wysiwygId . 'PollTab') + WysiwygTabFormContainer::create($this->wysiwygId . 'PollTab') ->label('wcf.poll.management') + ->setIcon('chart-bar') ->appendChild($this->pollContainer), ]), ]); diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php index dfefc168fd..2319f072c8 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php @@ -76,4 +76,12 @@ public function populate() $this->addClass('messageTabMenu'); } } + + /** + * @see WysiwygTabFormContainer::getIcon() + */ + public function getIcon(): string + { + return 'face-smile'; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php new file mode 100644 index 0000000000..1472e9bb82 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php @@ -0,0 +1,36 @@ + + * @since 6.2 + */ +class WysiwygTabFormContainer extends TabFormContainer +{ + protected ?string $icon = null; + + /** + * Gets the icon associated with the tab. + */ + public function getIcon(): ?string + { + return $this->icon; + } + + /** + * Sets the icon associated with the tab. + */ + public function setIcon(?string $icon): static + { + $this->icon = $icon; + + return $this; + } +} From 9e546bd2cf7dc7b85550e5c94283afb8fc0adf55 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 15 Jan 2025 11:40:25 +0100 Subject: [PATCH 46/65] Deprecate functions no longer required in the FormBuilder for quotes. Implement new class `IWysiwygTabFormContainer` which contains an icon for display and the information of an internal name --- .../templates/shared_wysiwygFormField.tpl | 23 ----- .../shared_wysiwygQuoteFormContainer.tpl | 16 ++++ .../shared_wysiwygTabFormContainer.tpl | 15 ++++ .../shared_wysiwygTabMenuFormContainer.tpl | 2 +- .../Core/Component/Message/MessageTabMenu.ts | 9 ++ ts/WoltLabSuite/Core/Component/Quote/List.ts | 8 +- .../Field/Dependency/Container/WysiwygTab.ts | 48 +++++++++++ .../Core/Component/Message/MessageTabMenu.js | 7 ++ .../WoltLabSuite/Core/Component/Quote/List.js | 8 +- .../Field/Dependency/Container/WysiwygTab.js | 46 ++++++++++ .../IWysiwygTabFormContainer.class.php | 26 ++++++ .../wysiwyg/WysiwygFormContainer.class.php | 79 ++++++----------- .../WysiwygQuoteFormContainer.class.php | 29 +++++++ .../WysiwygSmileyFormContainer.class.php | 12 ++- .../wysiwyg/WysiwygTabFormContainer.class.php | 31 ++++++- .../field/wysiwyg/WysiwygFormField.class.php | 86 ++----------------- 16 files changed, 269 insertions(+), 176 deletions(-) create mode 100644 com.woltlab.wcf/templates/shared_wysiwygQuoteFormContainer.tpl create mode 100644 com.woltlab.wcf/templates/shared_wysiwygTabFormContainer.tpl create mode 100644 ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTab.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/WysiwygTab.js create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/IWysiwygTabFormContainer.class.php create mode 100644 wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygQuoteFormContainer.class.php diff --git a/com.woltlab.wcf/templates/shared_wysiwygFormField.tpl b/com.woltlab.wcf/templates/shared_wysiwygFormField.tpl index 46eacccdd5..b95afd3b63 100644 --- a/com.woltlab.wcf/templates/shared_wysiwygFormField.tpl +++ b/com.woltlab.wcf/templates/shared_wysiwygFormField.tpl @@ -14,26 +14,3 @@ *}>{$field->getValue()} {include file='shared_wysiwyg' wysiwygSelector=$field->getPrefixedId()} - -{if $field->supportsQuotes()} - -{/if} diff --git a/com.woltlab.wcf/templates/shared_wysiwygQuoteFormContainer.tpl b/com.woltlab.wcf/templates/shared_wysiwygQuoteFormContainer.tpl new file mode 100644 index 0000000000..94d92f3fda --- /dev/null +++ b/com.woltlab.wcf/templates/shared_wysiwygQuoteFormContainer.tpl @@ -0,0 +1,16 @@ +
    + + + +{include file='shared_formContainerDependencies'} + + diff --git a/com.woltlab.wcf/templates/shared_wysiwygTabFormContainer.tpl b/com.woltlab.wcf/templates/shared_wysiwygTabFormContainer.tpl new file mode 100644 index 0000000000..a03a8a8465 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_wysiwygTabFormContainer.tpl @@ -0,0 +1,15 @@ +
    getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}{if !$container->checkDependencies()} hidden{/if}{* +*}> + {include file='shared_formContainerChildren'} +
    + +{include file='shared_formContainerDependencies'} + + diff --git a/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl b/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl index d477f05618..869543e1f0 100644 --- a/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl +++ b/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl @@ -6,7 +6,7 @@
      {foreach from=$container item='child'} {if $child->isAvailable()} -
    • checkDependencies()} hidden{/if}> +
    • checkDependencies()} hidden{/if}>
    \ No newline at end of file + {include file='__messageFormQuote'} +
    From 6cbabeaaa85873f7f30d9f57066d72308ebaba2c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 15 Jan 2025 12:22:04 +0100 Subject: [PATCH 50/65] Insert return statement again --- .../builder/container/wysiwyg/WysiwygFormContainer.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php index 42bd7f9754..3f3a9cce55 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php @@ -577,6 +577,7 @@ public function preselect($preselect = 'true') */ public function quoteData($objectType, $actionClass, array $selectors = []) { + return $this; } /** From 56ff1a604f27ec3d7a57b3f931285cdbd4d80623 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 15 Jan 2025 12:23:49 +0100 Subject: [PATCH 51/65] Insert return statement again --- .../system/form/builder/field/wysiwyg/WysiwygFormField.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php index 19cb43056a..1f98ffff1b 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php @@ -236,6 +236,7 @@ function (IFormDocument $document, array $parameters) { */ public function quoteData($objectType, $actionClass, array $selectors = []) { + return $this; } /** From 0a1facfe50ddaead40420b161c55053c356dceef Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 15 Jan 2025 13:03:02 +0100 Subject: [PATCH 52/65] Fixes the problem that the same messages were inserted multiple times --- ts/WoltLabSuite/Core/Component/Quote/Storage.ts | 2 +- .../files/js/WoltLabSuite/Core/Component/Quote/Storage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index 50037472ec..a26b5e17d9 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -221,7 +221,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 (JSON.stringify(q) === JSON.stringify(quote)) { + if ((q.rawMessage !== undefined && q.rawMessage === quote.rawMessage) || q.message === quote.message) { return 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 a6ed936665..2791c44ac9 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -157,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 (JSON.stringify(q) === JSON.stringify(quote)) { + if ((q.rawMessage !== undefined && q.rawMessage === quote.rawMessage) || q.message === quote.message) { return uuid; } } From b16ff7c062b36754a37ab73d593b8bf0b5859a96 Mon Sep 17 00:00:00 2001 From: Olaf Braun Date: Wed, 15 Jan 2025 14:12:42 +0100 Subject: [PATCH 53/65] Mark variables that are no longer used --- ts/WoltLabSuite/Core/Ui/Message/Quote.ts | 6 +++--- .../install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index 491e974740..a3dad998ce 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -17,13 +17,13 @@ export class UiMessageQuote { * Initializes the quote handler for given object type. */ constructor( - quoteManager: WCFMessageQuoteManager, + _quoteManager: WCFMessageQuoteManager, className: string, objectType: string, containerSelector: string, messageBodySelector: string, - messageContentSelector: string, - supportDirectInsert: boolean, + _messageContentSelector: string, + _supportDirectInsert: boolean, ) { // remove "Action" from className if (className.endsWith("Action")) { 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 9829985755..0d65a51114 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -11,7 +11,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func /** * Initializes the quote handler for given object type. */ - constructor(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) { + constructor(_quoteManager, className, objectType, containerSelector, messageBodySelector, _messageContentSelector, _supportDirectInsert) { // remove "Action" from className if (className.endsWith("Action")) { className = className.substring(0, className.length - 6); From 634775b30a9feecd4b7b64459b74900eb83e4808 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 16 Jan 2025 10:07:38 +0100 Subject: [PATCH 54/65] Cache used quotes that belong to a message that has not yet been saved. E.g. there were invalid entries in a form. --- .../templates/headIncludeJavaScript.tpl | 6 +++- ts/WoltLabSuite/Core/BootstrapFrontend.ts | 10 ++++++ ts/WoltLabSuite/Core/Component/Quote/List.ts | 4 ++- .../js/WoltLabSuite/Core/BootstrapFrontend.js | 23 +++++++++---- .../WoltLabSuite/Core/Component/Quote/List.js | 2 +- .../quote/MessageQuoteManager.class.php | 34 ++++++++++++++++++- 6 files changed, 68 insertions(+), 11 deletions(-) diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index c3c8e91ffe..12170f5a41 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -102,7 +102,11 @@ window.addEventListener('pageshow', function(event) { ], {/if} styleChanger: {if $__wcf->getStyleHandler()->showStyleChanger()}true{else}false{/if}, - {if $__wcf->user->userID}removeQuotes: [{implode from=$__wcf->getMessageQuoteManager()->getRemoveQuoteIDs() item=uuid}'{$uuid|encodeJS}'{/implode}],{/if} + {if $__wcf->user->userID && !$__wcf->getMessageQuoteManager()->getRemoveQuoteIDs()|empty}removeQuotes: [{implode from=$__wcf->getMessageQuoteManager()->getRemoveQuoteIDs() item=uuid}'{$uuid|encodeJS}'{/implode}],{/if} + {if $__wcf->user->userID && !$__wcf->getMessageQuoteManager()->getUsedQuotes()|empty}usedQuotes: new Map([ + {foreach from=$__wcf->getMessageQuoteManager()->getUsedQuotes() key=editorID item=uuids}['{$editorID|encodeJS}', [{implode from=$uuids item=uuid}'{$uuid|encodeJS}'{/implode}]]{/foreach} + ]), + {/if} }); }); diff --git a/ts/WoltLabSuite/Core/BootstrapFrontend.ts b/ts/WoltLabSuite/Core/BootstrapFrontend.ts index b7ccc37040..e599227d8f 100644 --- a/ts/WoltLabSuite/Core/BootstrapFrontend.ts +++ b/ts/WoltLabSuite/Core/BootstrapFrontend.ts @@ -38,6 +38,7 @@ interface BootstrapOptions { shareButtonProviders?: ShareProvider[]; styleChanger: boolean; removeQuotes?: string[]; + usedQuotes?: Map; } /** @@ -91,6 +92,15 @@ export function setup(options: BootstrapOptions): void { if (options.removeQuotes?.length) { void import("./Component/Quote/Storage").then(({ removeQuotes }) => removeQuotes(options.removeQuotes!)); } + if (options.usedQuotes?.size) { + void import("./Component/Quote/Storage").then(({ markQuoteAsUsed }) => { + options.usedQuotes!.forEach((uuids, editorId) => { + for (const uuid of uuids) { + markQuoteAsUsed(editorId, uuid); + } + }); + }); + } UiPageHeaderMenu.init(); diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 820238593e..dcfa6fd67e 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -114,7 +114,9 @@ class QuoteList { getUsedQuotes(this.#editorId).forEach((uuid) => { formSubmit.append( - DomUtil.createFragmentFromHtml(``), + DomUtil.createFragmentFromHtml( + ``, + ), ); }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js index c019d4e72b..84efa2e46e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js @@ -62,9 +62,18 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui if (options.removeQuotes?.length) { void new Promise((resolve_3, reject_3) => { require(["./Component/Quote/Storage"], resolve_3, reject_3); }).then(tslib_1.__importStar).then(({ removeQuotes }) => removeQuotes(options.removeQuotes)); } + if (options.usedQuotes?.size) { + void new Promise((resolve_4, reject_4) => { require(["./Component/Quote/Storage"], resolve_4, reject_4); }).then(tslib_1.__importStar).then(({ markQuoteAsUsed }) => { + options.usedQuotes.forEach((uuids, editorId) => { + for (const uuid of uuids) { + markQuoteAsUsed(editorId, uuid); + } + }); + }); + } UiPageHeaderMenu.init(); if (options.styleChanger) { - void new Promise((resolve_4, reject_4) => { require(["./Controller/Style/Changer"], resolve_4, reject_4); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => { + void new Promise((resolve_5, reject_5) => { require(["./Controller/Style/Changer"], resolve_5, reject_5); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => { ControllerStyleChanger.setup(); }); } @@ -99,22 +108,22 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui } } (0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => { - void new Promise((resolve_5, reject_5) => { require(["./Ui/Reaction/SummaryDetails"], resolve_5, reject_5); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_6, reject_6) => { require(["./Ui/Reaction/SummaryDetails"], resolve_6, reject_6); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment", () => { - void new Promise((resolve_6, reject_6) => { require(["./Component/Comment/woltlab-core-comment"], resolve_6, reject_6); }).then(tslib_1.__importStar); + void new Promise((resolve_7, reject_7) => { require(["./Component/Comment/woltlab-core-comment"], resolve_7, reject_7); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment-response", () => { - void new Promise((resolve_7, reject_7) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_7, reject_7); }).then(tslib_1.__importStar); + void new Promise((resolve_8, reject_8) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_8, reject_8); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-emoji-picker", () => { - void new Promise((resolve_8, reject_8) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_8, reject_8); }).then(tslib_1.__importStar); + void new Promise((resolve_9, reject_9) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_9, reject_9); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("[data-follow-user]", () => { - void new Promise((resolve_9, reject_9) => { require(["./Component/User/Follow"], resolve_9, reject_9); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_10, reject_10) => { require(["./Component/User/Follow"], resolve_10, reject_10); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("[data-ignore-user]", () => { - void new Promise((resolve_10, reject_10) => { require(["./Component/User/Ignore"], resolve_10, reject_10); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_11, reject_11) => { require(["./Component/User/Ignore"], resolve_11, reject_11); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); } }); 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 fe19aa4094..42ea0a3efd 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -90,7 +90,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve #formSubmitted() { const formSubmit = this.#editor.closest("form").querySelector(".formSubmit"); (0, Storage_1.getUsedQuotes)(this.#editorId).forEach((uuid) => { - formSubmit.append(Util_1.default.createFragmentFromHtml(``)); + formSubmit.append(Util_1.default.createFragmentFromHtml(``)); }); } } 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 726522dfbc..2fa2c0b3c1 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -24,6 +24,13 @@ class MessageQuoteManager extends SingletonFactory */ protected array $removeQuoteIDs = []; + /** + * list of quote that was used in the current request + * + * @var array + */ + protected array $usedQuotes = []; + /** * @inheritDoc */ @@ -295,9 +302,14 @@ public function readFormParameters(): void { if (isset($_REQUEST['__removeQuoteIDs']) && \is_array($_REQUEST['__removeQuoteIDs'])) { $quoteIDs = ArrayUtil::trim($_REQUEST['__removeQuoteIDs']); + foreach ($quoteIDs as $editorID => $uuids) { + if (!\is_string($editorID) || !\is_array($uuids)) { + unset($quoteIDs[$editorID]); + } + } if (!empty($quoteIDs)) { - $this->removeQuoteIDs = \array_merge($this->removeQuoteIDs, $quoteIDs); + $this->usedQuotes = \array_merge($this->usedQuotes, $quoteIDs); } } } @@ -307,6 +319,12 @@ public function readFormParameters(): void */ public function saved(): void { + foreach ($this->usedQuotes as $quoteIDs) { + $this->removeQuoteIDs = \array_merge($this->removeQuoteIDs, $quoteIDs); + } + + $this->usedQuotes = []; + $this->updateSession(); } @@ -365,6 +383,20 @@ public function getRemoveQuoteIDs(): array return $this->removeQuoteIDs; } + /** + * Returns the list of quote uuids that are used in the current request, + * but the creation of the message wasn't successful. + * This means that these others are only marked as having been used + * and only deleted when the message has been successfully saved. + * + * @return array + * @since 6.2 + */ + public function getUsedQuotes(): array + { + return $this->usedQuotes; + } + /** * Resets the list of quote uuids to be removed. * From 415d93df15018f52cde3bd5c43f2e90c709386d3 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 16 Jan 2025 10:07:58 +0100 Subject: [PATCH 55/65] Remove `MessageQuoteAction` --- .../lib/action/MessageQuoteAction.class.php | 224 ------------------ 1 file changed, 224 deletions(-) delete mode 100644 wcfsetup/install/files/lib/action/MessageQuoteAction.class.php diff --git a/wcfsetup/install/files/lib/action/MessageQuoteAction.class.php b/wcfsetup/install/files/lib/action/MessageQuoteAction.class.php deleted file mode 100644 index eee83f8e0c..0000000000 --- a/wcfsetup/install/files/lib/action/MessageQuoteAction.class.php +++ /dev/null @@ -1,224 +0,0 @@ - - */ -final class MessageQuoteAction extends AJAXProxyAction -{ - /** - * indicates if the WCF.Message.Quote.Manager object requesting data has any - * quote handlers which require updated object ids of full quotes - * @var int - */ - public $_getFullQuoteObjectIDs = false; - - /** - * list of quote ids - * @var string[] - */ - public $quoteIDs = []; - - /** - * list of object types - * @var string[] - */ - public $objectTypes = []; - - /** - * @inheritDoc - */ - public function readParameters() - { - AbstractSecureAction::readParameters(); - - if (isset($_POST['actionName'])) { - $this->actionName = StringUtil::trim($_POST['actionName']); - - if ( - !\in_array( - $this->actionName, - ['count', 'getQuotes', 'markForRemoval', 'remove', 'removeMarkedQuotes'] - ) - ) { - throw new UserInputException('actionName', 'invalid'); - } - } else { - throw new UserInputException('actionName'); - } - if (isset($_POST['getFullQuoteObjectIDs'])) { - $this->_getFullQuoteObjectIDs = \intval($_POST['getFullQuoteObjectIDs']); - } - if (isset($_POST['objectTypes']) && \is_array($_POST['objectTypes'])) { - $this->objectTypes = ArrayUtil::trim($_POST['objectTypes']); - } - if (isset($_POST['quoteIDs'])) { - $this->quoteIDs = ArrayUtil::trim($_POST['quoteIDs']); - - // validate quote ids - foreach ($this->quoteIDs as $key => $quoteID) { - if (MessageQuoteManager::getInstance()->getQuote($quoteID) === null) { - unset($this->quoteIDs[$key]); - } - } - } - } - - /** - * @inheritDoc - */ - public function execute() - { - AbstractAction::execute(); - - $returnValues = null; - switch ($this->actionName) { - case 'count': - $returnValues = [ - 'count' => $this->count(), - ]; - break; - - case 'getQuotes': - $returnValues = [ - 'template' => $this->getQuotes(), - ]; - break; - - case 'markForRemoval': - $this->markForRemoval(); - break; - - case 'remove': - $returnValues = [ - 'count' => $this->remove(), - ]; - break; - - case 'removeMarkedQuotes': - $returnValues = [ - 'count' => $this->removeMarkedQuotes(), - ]; - break; - - default: - throw new SystemException("Unknown action '" . $this->actionName . "'"); - break; - } - - if (\is_array($returnValues) && $this->_getFullQuoteObjectIDs) { - $returnValues['fullQuoteObjectIDs'] = $this->getFullQuoteObjectIDs(); - } - - $this->executed(); - - // force session update - WCF::getSession()->update(); - WCF::getSession()->disableUpdate(); - - if ($returnValues !== null) { - return new JsonResponse($returnValues); - } else { - return new EmptyResponse(200); - } - } - - /** - * Returns the count of stored quotes. - * - * @return int - */ - protected function count() - { - return MessageQuoteManager::getInstance()->countQuotes(); - } - - /** - * Returns the quote list template. - * - * @return string - */ - protected function getQuotes() - { - $supportPaste = isset($_POST['supportPaste']) ? (bool)$_POST['supportPaste'] : false; - - return MessageQuoteManager::getInstance()->getQuotes($supportPaste); - } - - /** - * @deprecated 5.5 This method is no longer used since 3.0. - */ - protected function markForRemoval() - { - if (!empty($this->quoteIDs)) { - MessageQuoteManager::getInstance()->markQuotesForRemoval($this->quoteIDs); - } - } - - /** - * Removes a list of quotes from storage and returns the remaining count. - * - * @return int - * @throws SystemException - * @throws UserInputException - */ - protected function remove() - { - if (empty($this->quoteIDs)) { - throw new UserInputException('quoteIDs'); - } - - foreach ($this->quoteIDs as $quoteID) { - if (!MessageQuoteManager::getInstance()->removeQuote($quoteID)) { - throw new SystemException("Unable to remove quote identified by '" . $quoteID . "'"); - } - } - - return $this->count(); - } - - /** - * Removes all quotes marked for removal and returns the remaining count. - * - * @return int - */ - protected function removeMarkedQuotes() - { - MessageQuoteManager::getInstance()->removeMarkedQuotes(); - - return $this->count(); - } - - /** - * Returns a list of full quotes by object ids for given object types. - * - * @return array - * @throws UserInputException - */ - protected function getFullQuoteObjectIDs() - { - if (empty($this->objectTypes)) { - throw new UserInputException('objectTypes'); - } - - try { - return MessageQuoteManager::getInstance()->getFullQuoteObjectIDs($this->objectTypes); - } catch (SystemException $e) { - throw new UserInputException('objectTypes'); - } - } -} From 711defc58ed6863ff5b871003fe12d9b8ca10413 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 16 Jan 2025 12:30:53 +0100 Subject: [PATCH 56/65] Remove quote object type interface --- com.woltlab.wcf/objectTypeDefinition.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 775710634f..5887bd8a1d 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -27,7 +27,6 @@ com.woltlab.wcf.message.quote - wcf\system\message\quote\IMessageQuoteHandler com.woltlab.wcf.user.recentActivityEvent From d4a183777fb5940a912bbeb846985cb3a661c75a Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 22 Jan 2025 13:08:35 +0100 Subject: [PATCH 57/65] Add missing line-breaks at EOF --- com.woltlab.wcf/templates/__messageFormQuote.tpl | 2 +- com.woltlab.wcf/templates/messageFormTabs.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/com.woltlab.wcf/templates/__messageFormQuote.tpl b/com.woltlab.wcf/templates/__messageFormQuote.tpl index 16c489f614..54fb4352fe 100644 --- a/com.woltlab.wcf/templates/__messageFormQuote.tpl +++ b/com.woltlab.wcf/templates/__messageFormQuote.tpl @@ -5,4 +5,4 @@ require(["WoltLabSuite/Core/Component/Quote/List"], ({ setup }) => { setup("{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}"); }); - \ No newline at end of file + diff --git a/com.woltlab.wcf/templates/messageFormTabs.tpl b/com.woltlab.wcf/templates/messageFormTabs.tpl index 90b8083122..c17ae4efc9 100644 --- a/com.woltlab.wcf/templates/messageFormTabs.tpl +++ b/com.woltlab.wcf/templates/messageFormTabs.tpl @@ -62,4 +62,4 @@ {event name='tabMenuContents'} {include file='__messageFormQuote'} -
    \ No newline at end of file +
    From b098421e58931b32796662e23e24fc9b90194c0b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 13:20:13 +0100 Subject: [PATCH 58/65] Rename endpoint to `/core/messages/message-author` --- ts/WoltLabSuite/Core/Api/Messages/Author.ts | 2 +- .../controller/core/messages/GetMessageAuthor.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/WoltLabSuite/Core/Api/Messages/Author.ts b/ts/WoltLabSuite/Core/Api/Messages/Author.ts index 2285cd63b6..593256b446 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/Author.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/Author.ts @@ -22,7 +22,7 @@ type Response = { }; export async function messageAuthor(className: string, objectID: number): Promise> { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/messageauthor"); + const url = new URL(window.WSC_RPC_API_URL + "core/messages/message-author"); url.searchParams.set("className", className); url.searchParams.set("objectID", objectID.toString()); 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 a6022686fc..e292ce4402 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 @@ -19,7 +19,7 @@ * @license GNU Lesser General Public License * @since 6.2 */ -#[GetRequest('/core/messages/messageauthor')] +#[GetRequest('/core/messages/message-author')] final class GetMessageAuthor implements IController { #[\Override] From 88acdb6ae9fd5e0c66f4f942de8b6cd2933bde7c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 13:20:36 +0100 Subject: [PATCH 59/65] Use `FontAwesomeIcon` instead of a string --- .../templates/shared_wysiwygTabMenuFormContainer.tpl | 2 +- .../container/wysiwyg/IWysiwygTabFormContainer.class.php | 3 ++- .../container/wysiwyg/WysiwygFormContainer.class.php | 7 ++++--- .../container/wysiwyg/WysiwygQuoteFormContainer.class.php | 4 +++- .../container/wysiwyg/WysiwygSmileyFormContainer.class.php | 5 +++-- .../container/wysiwyg/WysiwygTabFormContainer.class.php | 7 ++++--- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl b/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl index 869543e1f0..7c6ce1313f 100644 --- a/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl +++ b/com.woltlab.wcf/templates/shared_wysiwygTabMenuFormContainer.tpl @@ -8,7 +8,7 @@ {if $child->isAvailable()}
  • checkDependencies()} hidden{/if}>
  • diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/IWysiwygTabFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/IWysiwygTabFormContainer.class.php index 1e2a4effdc..92760f7615 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/IWysiwygTabFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/IWysiwygTabFormContainer.class.php @@ -3,6 +3,7 @@ namespace wcf\system\form\builder\container\wysiwyg; use wcf\system\form\builder\container\IFormContainer; +use wcf\system\style\FontAwesomeIcon; /** * Represents a container that is a tab of a wysiwyg tab menu. @@ -17,7 +18,7 @@ interface IWysiwygTabFormContainer extends IFormContainer /** * Gets the icon associated with the tab. */ - public function getIcon(): ?string; + public function getIcon(): ?FontAwesomeIcon; /** * Gets the name associated with the tab. diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php index 3f3a9cce55..99d9eebdc7 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygFormContainer.class.php @@ -14,6 +14,7 @@ use wcf\system\form\builder\field\wysiwyg\WysiwygFormField; use wcf\system\form\builder\IFormChildNode; use wcf\system\form\builder\TWysiwygFormNode; +use wcf\system\style\FontAwesomeIcon; /** * Represents the whole container with a WYSIWYG editor and the associated tab menu below it with @@ -505,7 +506,7 @@ public function populate() ->addClass('formAttachmentContent') ->label('wcf.attachment.attachments') ->name("attachments") - ->icon('paperclip') + ->icon(FontAwesomeIcon::fromValues('paperclip')) ->wysiwygId($this->getWysiwygId()) ->appendChild( FormContainer::create($this->wysiwygId . 'AttachmentsContainer') @@ -514,14 +515,14 @@ public function populate() WysiwygTabFormContainer::create($this->wysiwygId . 'SettingsTab') ->label('wcf.message.settings') - ->icon('gear') + ->icon(FontAwesomeIcon::fromValues('gear')) ->name('settings') ->wysiwygId($this->getWysiwygId()) ->appendChild($this->settingsContainer), WysiwygTabFormContainer::create($this->wysiwygId . 'PollTab') ->label('wcf.poll.management') - ->icon('chart-bar') + ->icon(FontAwesomeIcon::fromValues('chart-bar')) ->name('poll') ->wysiwygId($this->getWysiwygId()) ->appendChild($this->pollContainer), diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygQuoteFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygQuoteFormContainer.class.php index 2d1df6d3e0..be8474f842 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygQuoteFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygQuoteFormContainer.class.php @@ -2,6 +2,8 @@ namespace wcf\system\form\builder\container\wysiwyg; +use wcf\system\style\FontAwesomeIcon; + /** * Represents the form container for the quote-related fields below a WYSIWYG editor. * @@ -16,7 +18,7 @@ class WysiwygQuoteFormContainer extends WysiwygTabFormContainer public function __construct() { - $this->icon('quote-left') + $this->icon(FontAwesomeIcon::fromValues('quote-left')) ->name('quotes') ->label('wcf.bbcode.quote'); } diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php index 0823e21b8c..83c8ba9b9c 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php @@ -8,6 +8,7 @@ use wcf\system\form\builder\container\TabTabMenuFormContainer; use wcf\system\form\builder\TWysiwygFormNode; use wcf\system\form\builder\wysiwyg\WysiwygSmileyFormNode; +use wcf\system\style\FontAwesomeIcon; use wcf\util\StringUtil; /** @@ -78,9 +79,9 @@ public function populate() } #[\Override] - public function getIcon(): string + public function getIcon(): ?FontAwesomeIcon { - return 'face-smile'; + return FontAwesomeIcon::fromValues('face-smile'); } #[\Override] diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php index 0dce146f07..68923c3491 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabFormContainer.class.php @@ -4,6 +4,7 @@ use wcf\system\form\builder\container\TabFormContainer; use wcf\system\form\builder\TWysiwygFormNode; +use wcf\system\style\FontAwesomeIcon; /** * Represents a container that is a tab of a wysiwyg tab menu. @@ -22,11 +23,11 @@ class WysiwygTabFormContainer extends TabFormContainer implements IWysiwygTabFor */ protected $templateName = 'shared_wysiwygTabFormContainer'; - protected ?string $icon = null; + protected ?FontAwesomeIcon $icon = null; protected string $name = ''; #[\Override] - public function getIcon(): ?string + public function getIcon(): ?FontAwesomeIcon { return $this->icon; } @@ -34,7 +35,7 @@ public function getIcon(): ?string /** * Sets the icon associated with the tab. */ - public function icon(?string $icon): static + public function icon(?FontAwesomeIcon $icon): static { $this->icon = $icon; From 8a17b55b20858f207a757e2caee11a4a53a23412 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 13:22:29 +0100 Subject: [PATCH 60/65] Rename endpoint to `/core/messages/render-quote` --- ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts | 2 +- .../endpoint/controller/core/messages/RenderQuote.class.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index 179633d3b2..f426872ea7 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -28,7 +28,7 @@ export async function renderQuote( className: string, objectID: number, ): Promise> { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/renderquote"); + 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"); 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 c52807261a..c400e646d4 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 @@ -22,8 +22,7 @@ * @license GNU Lesser General Public License * @since 6.2 */ - -#[GetRequest('/core/messages/renderquote')] +#[GetRequest('/core/messages/render-quote')] final class RenderQuote implements IController { #[\Override] From 72b50a620833b62746479cd3d3bfcc5e0b18e627 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 13:22:37 +0100 Subject: [PATCH 61/65] Run `tsc` --- .../files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aed4fcbe42..814bdb881d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js @@ -12,7 +12,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu Object.defineProperty(exports, "__esModule", { value: true }); exports.renderQuote = renderQuote; async function renderQuote(objectType, className, objectID) { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/renderquote"); + 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"); From d96a6289a85559b8158bfa59706af551c9c18d05 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 13:27:13 +0100 Subject: [PATCH 62/65] Remove not used language phrases --- .../listener/PreloadPhrasesCollectingListener.class.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index a39c346e0d..b80ecb428c 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -136,13 +136,8 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.message.share.permalink.html'); $event->preload('wcf.message.share.socialMedia'); - $event->preload('wcf.message.quote.insertAllQuotes'); - $event->preload('wcf.message.quote.insertSelectedQuotes'); - $event->preload('wcf.message.quote.manageQuotes'); $event->preload('wcf.message.quote.quoteSelected'); $event->preload('wcf.message.quote.quoteAndReply'); - $event->preload('wcf.message.quote.removeAllQuotes'); - $event->preload('wcf.message.quote.showQuotes'); $event->preload('wcf.message.quote.insertQuote'); $event->preload('wcf.moderation.report.reportContent'); From 9e3408901231cc1b8bb983c97c17df4523457c23 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 14:00:18 +0100 Subject: [PATCH 63/65] Remove `WCF.Message.Quote`, `WCF.Message.Quote.Handler` and `WCF.Message.Quote.Manager` --- wcfsetup/install/files/js/WCF.Message.js | 681 ----------------------- 1 file changed, 681 deletions(-) diff --git a/wcfsetup/install/files/js/WCF.Message.js b/wcfsetup/install/files/js/WCF.Message.js index f369292f09..3806624fb0 100644 --- a/wcfsetup/install/files/js/WCF.Message.js +++ b/wcfsetup/install/files/js/WCF.Message.js @@ -1031,687 +1031,6 @@ else { }; } -/** - * Namespace for message quotes. - */ -WCF.Message.Quote = { }; - -if (COMPILER_TARGET_DEFAULT) { - /** - * Handles message quotes. - * - * @deprecated 5.4 Use `WoltLabSuite/Core/Ui/Message/Quote` instead - */ - WCF.Message.Quote.Handler = Class.extend({ - init: function (quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) { - require(["WoltLabSuite/Core/Ui/Message/Quote"], (UiMessageQuote) => { - new UiMessageQuote.default( - quoteManager, - className, - objectType, - containerSelector, - messageBodySelector, - messageContentSelector, - supportDirectInsert, - ); - }); - }, - }); - - /** - * Manages stored quotes. - * - * @param integer count - * - * @deprecated 6.2 - */ - WCF.Message.Quote.Manager = Class.extend({ - /** - * list of form buttons - * @var {Object} - */ - _buttons: {}, - - /** - * number of stored quotes - * @var {int} - */ - _count: 0, - - /** - * dialog overlay - * @var {jQuery} - */ - _dialog: null, - - /** - * editor element id - * @var {string} - */ - _editorId: '', - - /** - * alternative editor element id - * @var {string} - */ - _editorIdAlternative: '', - - /** - * form element - * @var {jQuery} - */ - _form: null, - - /** - * list of quote handlers - * @var {Object} - */ - _handlers: {}, - - /** - * true, if an up-to-date template exists - * @var {boolean} - */ - _hasTemplate: false, - - /** - * true, if related quotes should be inserted - * @var {boolean} - */ - _insertQuotes: true, - - /** - * action proxy - * @var {WCF.Action.Proxy} - */ - _proxy: null, - - /** - * list of quotes to remove upon submit - * @var {Array} - */ - _removeOnSubmit: [], - - /** - * allow pasting - * @var {boolean} - */ - _supportPaste: false, - - /** - * pasting was temporarily enabled due to an alternative editor being set - * @var boolean - */ - _supportPasteOverride: false, - - /** - * Initializes the quote manager. - * - * @param {int} count - * @param {string} elementID - * @param {boolean} supportPaste - * @param {Array} removeOnSubmit - */ - init: function (count, elementID, supportPaste, removeOnSubmit) { - this._buttons = { - insert: null, - remove: null - }; - this._count = parseInt(count) || 0; - this._dialog = null; - this._editorId = ''; - this._editorIdAlternative = ''; - this._form = null; - this._handlers = {}; - this._hasTemplate = false; - this._insertQuotes = true; - this._removeOnSubmit = []; - this._supportPaste = false; - this._supportPasteOverride = false; - - if (elementID) { - var element = $('#' + elementID); - if (element.length) { - this._editorId = elementID; - this._supportPaste = true; - - // get surrounding form-tag - this._form = element.parents('form:eq(0)'); - if (this._form.length) { - this._form.submit(this._submit.bind(this)); - this._removeOnSubmit = removeOnSubmit || []; - } - else { - this._form = null; - - // allow override - this._supportPaste = (supportPaste === true); - } - } - } - - this._proxy = new WCF.Action.Proxy({ - showLoadingOverlay: false, - success: $.proxy(this._success, this), - url: 'index.php?message-quote/&t=' + SECURITY_TOKEN - }); - - this._toggleShowQuotes(); - - WCF.System.Event.addListener('com.woltlab.wcf.quote', 'reload', this.countQuotes.bind(this)); - - // event forwarding - WCF.System.Event.addListener('com.woltlab.wcf.message.quote', 'insert', (function (data) { - const element = document.getElementById( - this._editorIdAlternative ? this._editorIdAlternative : this._editorId - ); - - require(["WoltLabSuite/Core/Component/Ckeditor/Event"], ({ dispatchToCkeditor }) => { - dispatchToCkeditor(element).insertQuote({ - author: data.quote.username, - content: data.quote.text, - isText: !data.quote.isFullQuote, - link: data.quote.link, - }); - }); - }).bind(this)); - }, - - /** - * Sets an alternative editor element id on runtime. - * - * @param {(string|jQuery)} elementId element id or jQuery element - */ - setAlternativeEditor: function (elementId) { - if (!this._editorIdAlternative && !this._supportPaste) { - this._hasTemplate = false; - this._supportPaste = true; - this._supportPasteOverride = true; - } - - if (typeof elementId === 'object') elementId = elementId[0].id; - this._editorIdAlternative = elementId; - }, - - /** - * Clears alternative editor element id. - */ - clearAlternativeEditor: function () { - if (this._supportPasteOverride) { - this._hasTemplate = false; - this._supportPaste = false; - this._supportPasteOverride = false; - } - - this._editorIdAlternative = ''; - }, - - /** - * Registers a quote handler. - * - * @param {string} objectType - * @param {WCF.Message.Quote.Handler} handler - */ - register: function (objectType, handler) { - this._handlers[objectType] = handler; - }, - - /** - * Updates number of stored quotes. - * - * @param {int} count - * @param {Object} fullQuoteObjectIDs - */ - updateCount: function (count, fullQuoteObjectIDs) { - this._count = parseInt(count) || 0; - - this._toggleShowQuotes(); - - // update full quote ids of handlers - for (var $objectType in this._handlers) { - if (this._handlers.hasOwnProperty($objectType)) { - var $objectIDs = fullQuoteObjectIDs[$objectType] || []; - this._handlers[$objectType].updateFullQuoteObjectIDs($objectIDs); - } - } - }, - - /** - * Inserts all associated quotes upon first time using quick reply. - * - * @param {string} className - * @param {int} parentObjectID - * @param {Object} callback - */ - insertQuotes: function (className, parentObjectID, callback) { - if (!this._insertQuotes) { - this._insertQuotes = true; - - return; - } - - new WCF.Action.Proxy({ - autoSend: true, - data: { - actionName: 'getRenderedQuotes', - className: className, - interfaceName: 'wcf\\data\\IMessageQuoteAction', - parameters: { - parentObjectID: parentObjectID - } - }, - success: callback - }); - }, - - /** - * Toggles the display of the 'Show quotes' button - */ - _toggleShowQuotes: function () { - require(['WoltLabSuite/Core/Ui/Page/Action'], (function (UiPageAction) { - var buttonName = 'showQuotes'; - - if (this._count) { - var button = UiPageAction.get(buttonName); - if (button === undefined) { - button = elCreate('a'); - button.addEventListener('mousedown', this._click.bind(this)); - - UiPageAction.add(buttonName, button); - } - - button.textContent = WCF.Language.get('wcf.message.quote.showQuotes', { - count: this._count - }); - - UiPageAction.show(buttonName); - } - else { - UiPageAction.remove(buttonName); - } - - this._hasTemplate = false; - }).bind(this)); - }, - - /** - * Handles clicks on 'Show quotes'. - */ - _click: function () { - if (this._hasTemplate) { - this._dialog.wcfDialog('open'); - } - else { - this._proxy.showLoadingOverlayOnce(); - - this._proxy.setOption('data', { - actionName: 'getQuotes', - supportPaste: this._supportPaste - }); - this._proxy.sendRequest(); - } - }, - - /** - * Renders the dialog. - * - * @param {string} template - */ - renderDialog: function (template) { - // create dialog if not exists - if (this._dialog === null) { - this._dialog = $('#messageQuoteList'); - if (!this._dialog.length) { - this._dialog = $('
    ').hide().appendTo(document.body); - } - } - - // add template - this._dialog.html(template); - - // add 'insert' and 'delete' buttons - var $formSubmit = $('
    ').appendTo(this._dialog); - if (this._supportPaste) this._buttons.insert = $('').click($.proxy(this._insertSelected, this)).appendTo($formSubmit); - this._buttons.remove = $('').click($.proxy(this._removeSelected, this)).appendTo($formSubmit); - - // show dialog - this._dialog.wcfDialog({ - title: WCF.Language.get('wcf.message.quote.manageQuotes') - }); - this._dialog.wcfDialog('render'); - this._hasTemplate = true; - - // bind event listener - var $insertQuoteButtons = this._dialog.find('.jsInsertQuote'); - if (this._supportPaste) { - $insertQuoteButtons.click($.proxy(this._insertQuote, this)); - } - else { - $insertQuoteButtons.hide(); - } - - this._dialog.find('input.jsCheckbox').change($.proxy(this._changeButtons, this)); - - // mark quotes for removal - if (this._removeOnSubmit.length) { - var self = this; - this._dialog.find('input.jsRemoveQuote').each(function (index, input) { - var $input = $(input).change($.proxy(this._change, this)); - - // mark for deletion - if (WCF.inArray($input.parent('li').attr('data-quote-id'), self._removeOnSubmit)) { - $input.attr('checked', 'checked'); - } - }); - } - }, - - /** - * Updates button labels if a checkbox is checked or unchecked. - */ - _changeButtons: function () { - // selection - if (this._dialog.find('input.jsCheckbox:checked').length) { - if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertSelectedQuotes')); - this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeSelectedQuotes')); - } - else { - // no selection, pick all - if (this._supportPaste) this._buttons.insert.html(WCF.Language.get('wcf.message.quote.insertAllQuotes')); - this._buttons.remove.html(WCF.Language.get('wcf.message.quote.removeAllQuotes')); - } - }, - - /** - * Checks for change event on delete-checkboxes. - * - * @param {Object} event - */ - _change: function (event) { - var $input = $(event.currentTarget); - var $quoteID = $input.parent('li').attr('data-quote-id'); - - if ($input.prop('checked')) { - this._removeOnSubmit.push($quoteID); - } - else { - var index = this._removeOnSubmit.indexOf($quoteID); - if (index !== -1) { - this._removeOnSubmit.splice(index, 1); - } - } - }, - - /** - * Inserts the selected quotes. - */ - _insertSelected: function () { - if (!this._dialog.find('input.jsCheckbox:checked').length) { - this._dialog.find('input.jsCheckbox').prop('checked', 'checked'); - } - - // close dialog - this._dialog.wcfDialog('close'); - - // insert all quotes - window.setTimeout(() => { - this._dialog.find('input.jsCheckbox:checked').each($.proxy(function (index, input) { - this._insertQuote(null, input); - }, this)); - }, 0); - }, - - /** - * Inserts a quote. - * - * @param {Event} event - * @param {Object} inputElement - */ - _insertQuote: function (event, inputElement) { - var listItem = $(event ? event.currentTarget : inputElement).parents('li:eq(0)'); - var text = listItem.children('.jsFullQuote')[0].textContent.trim(); - - var message = listItem.parents('.message:eq(0)'); - var author = message.data('username'); - var link = message.data('link'); - var isText = !elDataBool(listItem[0], 'is-full-quote'); - - const element = document.getElementById( - this._editorIdAlternative ? this._editorIdAlternative : this._editorId - ); - - require(["WoltLabSuite/Core/Component/Ckeditor/Event"], ({ dispatchToCkeditor }) => { - dispatchToCkeditor(element).insertQuote({ - author, - content: text, - isText, - link, - }); - }); - - // remove quote upon submit or upon request - this._removeOnSubmit.push(listItem.data('quote-id')); - - // close dialog - if (event !== null) { - require(["WoltLabSuite/Core/Environment"], (function (Environment) { - var callback = (function () { - this._dialog.wcfDialog("close"); - }).bind(this); - - // Slightly delay the closing of the overlay, preventing some unexpected - // changes to the scroll position on iOS. - if (Environment.platform() === "ios") { - window.setTimeout(callback, 100); - } else { - callback(); - } - }.bind(this))); - } - }, - - /** - * Removes selected quotes. - */ - _removeSelected: function () { - if (!this._dialog.find('input.jsCheckbox:checked').length) { - this._dialog.find('input.jsCheckbox').prop('checked', 'checked'); - } - - var $quoteIDs = []; - this._dialog.find('input.jsCheckbox:checked').each(function (index, input) { - $quoteIDs.push($(input).parents('li').attr('data-quote-id')); - }); - - if ($quoteIDs.length) { - // get object types - var $objectTypes = []; - for (var $objectType in this._handlers) { - if (this._handlers.hasOwnProperty($objectType)) { - $objectTypes.push($objectType); - } - } - - this._proxy.setOption('data', { - actionName: 'remove', - getFullQuoteObjectIDs: this._handlers.length > 0, - objectTypes: $objectTypes, - quoteIDs: $quoteIDs - }); - this._proxy.sendRequest(); - - this._dialog.wcfDialog('close'); - } - }, - - /** - * Appends list of quote ids to remove after successful submit. - */ - _submit: function () { - if (this._supportPaste && this._removeOnSubmit.length > 0) { - var $formSubmit = this._form.find('.formSubmit'); - for (var i = 0, length = this._removeOnSubmit.length; i < length; i++) { - $('').appendTo($formSubmit); - } - } - }, - - /** - * Returns a list of quote ids marked for removal. - * - * @return {Array} - */ - getQuotesMarkedForRemoval: function () { - return this._removeOnSubmit; - }, - - /** - * @deprecated 5.5 This method is no longer used since 3.0. - */ - markQuotesForRemoval: function () { - if (this._removeOnSubmit.length) { - this._proxy.setOption('data', { - actionName: 'markForRemoval', - quoteIDs: this._removeOnSubmit - }); - this._proxy.suppressErrors(); - this._proxy.sendRequest(); - } - }, - - /** - * Removes all marked quote ids. - */ - removeMarkedQuotes: function () { - if (this._removeOnSubmit.length) { - this._proxy.setOption('data', { - actionName: 'removeMarkedQuotes', - getFullQuoteObjectIDs: this._handlers.length > 0 - }); - this._proxy.sendRequest(); - } - }, - - /** - * Counts stored quotes. - */ - countQuotes: function () { - var $objectTypes = []; - for (var $objectType in this._handlers) { - if (this._handlers.hasOwnProperty($objectType)) { - $objectTypes.push($objectType); - } - } - - this._proxy.setOption('data', { - actionName: 'count', - getFullQuoteObjectIDs: ($objectTypes.length > 0), - objectTypes: $objectTypes - }); - this._proxy.sendRequest(); - }, - - /** - * Handles successful AJAX requests. - * - * @param {Object} data - */ - _success: function (data) { - if (data === null) { - return; - } - - if (data.count !== undefined) { - var $fullQuoteObjectIDs = (data.fullQuoteObjectIDs !== undefined) ? data.fullQuoteObjectIDs : {}; - this.updateCount(data.count, $fullQuoteObjectIDs); - } - - if (data.template !== undefined) { - if ($.trim(data.template) == '') { - this.updateCount(0, {}); - } - else { - this.renderDialog(data.template); - } - } - }, - - /** - * Returns true if pasting is supported. - * - * @return boolean - */ - supportPaste: function () { - return this._supportPaste; - } - }); -} -else { - WCF.Message.Quote.Handler = Class.extend({ - _activeContainerID: "", - _className: "", - _containers: {}, - _containerSelector: "", - _copyQuote: {}, - _message: "", - _messageBodySelector: "", - _objectID: 0, - _objectType: "", - _proxy: {}, - _quoteManager: {}, - init: function() {}, - _initContainers: function() {}, - _mouseDown: function() {}, - _getNodeText: function() {}, - _mouseUp: function() {}, - _normalize: function() {}, - _getBoundingRectangle: function() {}, - _initCopyQuote: function() {}, - _getSelectedText: function() {}, - _saveFullQuote: function() {}, - _saveQuote: function() {}, - _saveAndInsertQuote: function() {}, - _success: function() {}, - updateFullQuoteObjectIDs: function() {} - }); - - WCF.Message.Quote.Manager = Class.extend({ - _buttons: {}, - _count: 0, - _dialog: {}, - _editorId: "", - _editorIdAlternative: "", - _form: {}, - _handlers: {}, - _hasTemplate: false, - _insertQuotes: true, - _proxy: {}, - _removeOnSubmit: {}, - _supportPaste: false, - init: function() {}, - setAlternativeEditor: function() {}, - clearAlternativeEditor: function() {}, - register: function() {}, - updateCount: function() {}, - insertQuotes: function() {}, - _toggleShowQuotes: function() {}, - _click: function() {}, - renderDialog: function() {}, - _changeButtons: function() {}, - _change: function() {}, - _insertSelected: function() {}, - _insertQuote: function() {}, - _removeSelected: function() {}, - _submit: function() {}, - getQuotesMarkedForRemoval: function() {}, - markQuotesForRemoval: function() {}, - removeMarkedQuotes: function() {}, - countQuotes: function() {}, - _success: function() {}, - supportPaste: function() {} - }); -} - /** * Namespace for message sharing related classes. */ From 489995a004a1b0471fd6abaf03aa50f2b5c519ce Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 14:09:01 +0100 Subject: [PATCH 64/65] Remove unused laugage variables --- .../PreloadPhrasesCollectingListener.class.php | 1 + wcfsetup/install/lang/de.xml | 12 ++++++------ wcfsetup/install/lang/en.xml | 12 ++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index b80ecb428c..503adcdfd0 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -138,6 +138,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.message.quote.quoteSelected'); $event->preload('wcf.message.quote.quoteAndReply'); + $event->preload('wcf.message.quote.quoteMessage'); $event->preload('wcf.message.quote.insertQuote'); $event->preload('wcf.moderation.report.reportContent'); diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index e57287ce37..445413a35f 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -4246,16 +4246,10 @@ Erlaubte Dateiendungen: gif, jpg, jpeg, png, webp]]> - - - - - - @@ -7596,5 +7590,11 @@ Benachrichtigungen auf {PAGE_TITLE|phra + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index a8daa29069..508c609ace 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -4192,16 +4192,10 @@ Allowed extensions: gif, jpg, jpeg, png, webp]]> - - - - - - @@ -7487,5 +7481,11 @@ your notifications on {PAGE_TITLE|phras + + + + + + From ad33be7f533c2fb4d6cdffb750b25af8a5c5426d Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 22 Jan 2025 14:09:50 +0100 Subject: [PATCH 65/65] Run `tsc` --- .../install/files/js/WoltLabSuite/Core/Api/Messages/Author.js | 2 +- .../files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js index bb3051cc90..8f16cf02ea 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js @@ -12,7 +12,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu Object.defineProperty(exports, "__esModule", { value: true }); exports.messageAuthor = messageAuthor; async function messageAuthor(className, objectID) { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/messageauthor"); + 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; 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 814bdb881d..5d1c7fd484 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js @@ -12,7 +12,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu Object.defineProperty(exports, "__esModule", { value: true }); exports.renderQuote = renderQuote; async function renderQuote(objectType, className, objectID) { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); + 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");