Store information about the message author
authorCyperghost <olaf_schmitz_1@t-online.de>
Thu, 19 Dec 2024 10:56:38 +0000 (11:56 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 8 Jan 2025 16:25:19 +0000 (17:25 +0100)
ts/WoltLabSuite/Core/Api/Messages/Author.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts
ts/WoltLabSuite/Core/Component/Quote/Message.ts
ts/WoltLabSuite/Core/Component/Quote/Storage.ts
ts/WoltLabSuite/Core/Ui/Message/Quote.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js
wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php

diff --git a/ts/WoltLabSuite/Core/Api/Messages/Author.ts b/ts/WoltLabSuite/Core/Api/Messages/Author.ts
new file mode 100644 (file)
index 0000000..8a850bc
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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<ApiResult<Response>> {
+  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);
+}
index 8f389ff93063cf7a2a279c9d1b11282bd672dc06..11fda558e13dc6f534ee24907c255dbb7e0db764 100644 (file)
 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<ApiResult<Response>> {
+export async function renderQuote(
+  objectType: string,
+  className: string,
+  objectID: number,
+): Promise<ApiResult<Response>> {
   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;
index b0297ab22b4ac395532753268af015c13cec61f4..92fb91042ce73b75348f61b5829c0e0bea55c4f0 100644 (file)
@@ -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);
index 61bd25b83019071b423404ac0e3031cbed2084e4..78b74ba8540f1282c9ea48b976f321932f801fba 100644 (file)
 
 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<string, Set<string>>;
+  messages: Map<string, Message>;
 }
 
-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<string, Set<string>> {
@@ -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<string, Message>(value);
       }
 
       return value;
index 29b39a52d1aa02c18eae9d82c45dac3cbfbed998..491e9747403c47a773034a2f172a27b70d41b2a8 100644 (file)
@@ -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);
   }
 }
 
index e213a4cf2fde26c6283dac0bfe6ec473ad6ab87f..72f49b9485eafe5ed47d85441120f576e8881237 100644 (file)
@@ -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));
index 9399bb00c29089149b62bf3f0fc0dfc51d839975..982998575511b2298345ad0a1cfa83d80e90ae0d 100644 (file)
@@ -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;
index c93763849899467a3b6b0a18ae7b465f490e9326..8110ba1e3eefe4b25c703a6340c6d26e1a8ea216 100644 (file)
@@ -138,6 +138,7 @@ return static function (): void {
             $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 (file)
index 0000000..e378a79
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+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\IMessage;
+use wcf\http\Helper;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+
+/**
+ * Returns information about the author of a message.
+ *
+ * @author    Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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,
+    ) {
+    }
+}
index 76d6ba53cd93e05b6b9fcdc830381f1924f37a0c..e6868a7fbf936f214859acc882455158bdab4668 100644 (file)
@@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface;
 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 @@ use wcf\system\html\input\HtmlInputProcessor;
  * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @since     6.2
  */
+
 #[GetRequest('/core/messages/renderquote')]
 final class RenderQuote implements IController
 {
@@ -27,16 +29,27 @@ final class RenderQuote implements IController
     {
         $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,
     ) {
     }
 }