link: string;
title: string;
avatar: string;
- message: string;
+ message: string | null;
+ rawMessage: string | null;
};
export async function renderQuote(
* @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";
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}`)!;
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">
<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>`,
)
</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 {
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);
avatar: string;
}
+interface Quote {
+ message: string;
+ rawMessage?: string;
+}
+
interface StorageData {
- quotes: Map<string, Set<string>>;
+ quotes: Map<string, Set<Quote>>;
messages: Map<string, Message>;
}
return;
}
- storeQuote(objectType, result.value, message);
+ storeQuote(objectType, result.value, {
+ message,
+ });
refreshQuoteLists();
}
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;
}
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);
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);
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);
}
}
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">
<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("")}
</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 {
}
(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) => {
// TODO error handling
return;
}
- storeQuote(objectType, result.value, message);
+ storeQuote(objectType, result.value, {
+ message,
+ });
(0, List_1.refreshQuoteLists)();
}
async function saveFullQuote(objectType, objectClassName, objectId) {
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;
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() {
--- /dev/null
+<?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;
+}
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;
$userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID());
+ if ($object instanceof IEmbeddedMessageObject) {
+ $object->loadEmbeddedObjects();
+ }
+
return new JsonResponse(
[
"objectID" => $object->getObjectID(),
"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,
);
private function renderFullQuote(IMessage $object): string
{
- // TODO load embedded objects?
-
$htmlInputProcessor = new HtmlInputProcessor();
$htmlInputProcessor->processIntermediate($object->getMessage());
$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');