Add `IEmbeddedMessageObject` to load embedded object
authorCyperghost <olaf_schmitz_1@t-online.de>
Fri, 20 Dec 2024 13:41:10 +0000 (14:41 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 8 Jan 2025 16:25:19 +0000 (17:25 +0100)
ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts
ts/WoltLabSuite/Core/Component/Quote/List.ts
ts/WoltLabSuite/Core/Component/Quote/Storage.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js
wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php
wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php

index 85195f947447339e024b2e267f866b80f223418d..fde2cdc3776b6dd9ed379407cc7df81450ea6eaa 100644 (file)
@@ -19,7 +19,8 @@ type Response = {
   link: string;
   title: string;
   avatar: string;
-  message: string;
+  message: string | null;
+  rawMessage: string | null;
 };
 
 export async function renderQuote(
index 9445fa9578353b40e08b5afc652381c34404000b..8fe2d35578bb42a114f33114c0eca940e09b67dd 100644 (file)
@@ -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<string, QuoteList>();
 
 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(`<article class="message messageReduced jsInvalidQuoteTarget">
+      const fragment = DomUtil.createFragmentFromHtml(`<article class="message messageReduced jsInvalidQuoteTarget">
   <div class="messageContent">
     <header class="messageHeader">
       <div class="box32 messageHeaderWrapper">
@@ -74,11 +74,12 @@ class QuoteList {
   <span>
     <input type="checkbox" value="1" class="jsCheckbox">
     <button type="button" class="jsTooltip jsInsertQuote" title="${getPhrase("wcf.message.quote.insertQuote")}">
+        <fa-icon name="plus"></fa-icon>
     </button>
   </span>
   
   <div class="jsQuote">
-    ${quote}
+    ${quote.message}
   </div>
 </li>`,
           )
@@ -87,16 +88,29 @@ class QuoteList {
       </div>
     </div>
   </div>
-</article>`),
-      );
-      // TODO render quotes
+</article>`);
+
+      fragment.querySelectorAll<HTMLButtonElement>(".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);
index a4bdfbb545d4f54990b4c539a6820dfb0eb4aa20..314173fd44c0fd2fde9ba64082ed046967dacc81 100644 (file)
@@ -23,8 +23,13 @@ interface Message {
   avatar: string;
 }
 
+interface Quote {
+  message: string;
+  rawMessage?: string;
+}
+
 interface StorageData {
-  quotes: Map<string, Set<string>>;
+  quotes: Map<string, Set<Quote>>;
   messages: Map<string, Message>;
 }
 
@@ -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<string, Set<string>> {
+export function getQuotes(): Map<string, Set<Quote>> {
   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);
 }
index a8e1c59bf3d50ffac936efbfc8ad72c574ddb51a..d2dd115fc7d7aad0174f3688ffa409989fd6a186 100644 (file)
@@ -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(`<article class="message messageReduced jsInvalidQuoteTarget">
+                const fragment = Util_1.default.createFragmentFromHtml(`<article class="message messageReduced jsInvalidQuoteTarget">
   <div class="messageContent">
     <header class="messageHeader">
       <div class="box32 messageHeaderWrapper">
@@ -63,11 +65,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
   <span>
     <input type="checkbox" value="1" class="jsCheckbox">
     <button type="button" class="jsTooltip jsInsertQuote" title="${(0, Language_1.getPhrase)("wcf.message.quote.insertQuote")}">
+        <fa-icon name="plus"></fa-icon>
     </button>
   </span>
   
   <div class="jsQuote">
-    ${quote}
+    ${quote.message}
   </div>
 </li>`)
                     .join("")}
@@ -75,12 +78,24 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
       </div>
     </div>
   </div>
-</article>`));
-                // TODO render quotes
+</article>`);
+                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) => {
index fd6e14848dc2b3c7211b9e98191de034556a052c..b71b4f69bed386fd2bca76a801ecffc6b27125b3 100644 (file)
@@ -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 (file)
index 0000000..da483e1
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace wcf\data;
+
+/**
+ * Interface for embedded message objects.
+ *
+ * @author      Olaf Braun
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.2
+ */
+interface IEmbeddedMessageObject
+{
+    /**
+     * Loads embedded objects for the given object type and object IDs.
+     */
+    public function loadEmbeddedObjects(): void;
+}
index 21c1103cb2ceff6e6d6428de308ced7f9a701e51..aec19a6a4df910fb7372af111d79fbf8b760c616 100644 (file)
@@ -5,6 +5,7 @@ namespace wcf\system\endpoint\controller\core\messages;
 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 @@ final class RenderQuote implements IController
 
         $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID());
 
+        if ($object instanceof IEmbeddedMessageObject) {
+            $object->loadEmbeddedObjects();
+        }
+
         return new JsonResponse(
             [
                 "objectID" => $object->getObjectID(),
@@ -43,7 +48,8 @@ final class RenderQuote implements IController
                 "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 @@ final class RenderQuote implements IController
 
     private function renderFullQuote(IMessage $object): string
     {
-        // TODO load embedded objects?
-
         $htmlInputProcessor = new HtmlInputProcessor();
         $htmlInputProcessor->processIntermediate($object->getMessage());
 
index 8aaf4fe61206688ba8c3462970538e704c5aeebf..8cbac407ea18c1ab766d38cb5d8312fd096e6d87 100644 (file)
@@ -143,6 +143,7 @@ final class PreloadPhrasesCollectingListener
         $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');