--- /dev/null
+/**
+ * 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 = string;
+
+export async function renderQuote(objectType: 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("objectID", objectID.toString());
+
+ let response: Response;
+ try {
+ response = (await prepareRequest(url).get().fetchAsJson()) as Response;
+ } catch (e) {
+ return apiResultFromError(e);
+ }
+
+ return apiResultFromValue(response);
+}
* @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<string, QuoteList>();
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(
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<string, Container>();
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")) {
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<HTMLButtonElement>(".jsQuoteManagerQuoteAndInsert")!.hidden = !supportDirectInsert;
activeEditor = editor;
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");
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);
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();
+ }
+}
--- /dev/null
+/**
+ * Stores the quote data.
+ *
+ * @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 * as Core from "WoltLabSuite/Core/Core";
+import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote";
+
+interface StorageData {
+ quotes: Map<string, Set<string>>;
+}
+
+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<string, Set<string>> {
+ 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<string, Set<string>>(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;
+ }),
+ );
+}
messageContentSelector: string,
supportDirectInsert: boolean,
) {
- registerContainer(containerSelector, messageBodySelector);
+ registerContainer(containerSelector, messageBodySelector, objectType);
}
}
* @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;
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,
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);
}
});
* @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;
}
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");
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);
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();
+ }
+ }
});
* 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;
$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());
--- /dev/null
+<?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\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\html\input\HtmlInputProcessor;
+
+/**
+ * Renders a quote 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/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,
+ ) {
+ }
+}