Skip to content

Commit bd85086

Browse files
authored
Overhaul the quote API (#6412)
* (Ab)use the legacy API to retrieve data for quotes * Add support for legacy full quotes * Reduce the amount of data required for quotes These fields aren’t used anyway. * Use an event to collect the message being quoted * Remove the `message-author` endpoint It can be completely replaced by the existing `render-quote` endpoint. * Minor fixes * Reintroduce the quote handler to simplify message fetching * Unify the implementations for full quotes * Simplify the code a bit * Merge the partial quotes implementation, improve type safety
1 parent 03800cf commit bd85086

File tree

20 files changed

+447
-427
lines changed

20 files changed

+447
-427
lines changed

com.woltlab.wcf/objectTypeDefinition.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
</definition>
2828
<definition>
2929
<name>com.woltlab.wcf.message.quote</name>
30+
<interfacename>wcf\system\message\quote\IMessageQuoteHandler</interfacename>
3031
</definition>
3132
<definition>
3233
<name>com.woltlab.wcf.user.recentActivityEvent</name>

ts/WoltLabSuite/Core/Api/Messages/Author.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,21 @@ import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";
1313

1414
type Response = {
1515
objectID: number;
16-
authorID: number | null;
1716
author: string;
18-
time: string;
1917
link: string;
20-
title: string;
2118
avatar: string;
2219
message: string | null;
2320
rawMessage: string | null;
2421
};
2522

2623
export async function renderQuote(
2724
objectType: string,
28-
className: string,
2925
objectID: number,
26+
isFullQuote: boolean,
3027
): Promise<ApiResult<Response>> {
3128
const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote");
3229
url.searchParams.set("objectType", objectType);
33-
url.searchParams.set("className", className);
34-
url.searchParams.set("fullQuote", "true");
30+
url.searchParams.set("isFullQuote", String(isFullQuote));
3531
url.searchParams.set("objectID", objectID.toString());
3632

3733
let response: Response;

ts/WoltLabSuite/Core/Component/Quote/List.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,15 @@ class QuoteList {
7878
fragment.querySelector('button[data-action="insert"]')!.addEventListener("click", () => {
7979
markQuoteAsUsed(this.#editorId, uuid);
8080

81+
const content = quote.rawMessage || quote.message;
82+
if (content === null) {
83+
throw new Error("Expected either the `rawMessage` or `message` to be a string.");
84+
}
85+
8186
dispatchToCkeditor(this.#editor).insertQuote({
8287
author: message.author,
83-
content: quote.rawMessage === undefined ? quote.message : quote.rawMessage,
84-
isText: quote.rawMessage === undefined,
88+
content,
89+
isText: !quote.rawMessage,
8590
link: message.link,
8691
});
8792
});
@@ -142,21 +147,23 @@ export function setup(editorId: string, containerId?: string): void {
142147
throw new Error(`The editor '${editorId}' does not exist.`);
143148
}
144149

145-
listenToCkeditor(editor).ready(({ ckeditor }) => {
146-
if (ckeditor.features.quoteBlock) {
147-
quoteLists.set(editorId, new QuoteList(editorId, editor, containerId));
148-
}
149-
150-
if (ckeditor.isVisible()) {
151-
setActiveEditor(ckeditor, ckeditor.features.quoteBlock);
152-
}
150+
listenToCkeditor(editor)
151+
.ready(({ ckeditor }) => {
152+
if (ckeditor.features.quoteBlock) {
153+
quoteLists.set(editorId, new QuoteList(editorId, editor, containerId));
154+
}
153155

154-
ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => {
155-
if (isFocused) {
156+
if (ckeditor.isVisible()) {
156157
setActiveEditor(ckeditor, ckeditor.features.quoteBlock);
157158
}
159+
160+
ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => {
161+
if (isFocused) {
162+
setActiveEditor(ckeditor, ckeditor.features.quoteBlock);
163+
}
164+
});
165+
})
166+
.destroy(() => {
167+
removeActiveEditor(editor);
158168
});
159-
}).destroy(() => {
160-
removeActiveEditor(editor);
161-
});
162169
}

ts/WoltLabSuite/Core/Component/Quote/Message.ts

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@ import {
1717
getFullQuoteUuid,
1818
saveFullQuote,
1919
markQuoteAsUsed,
20-
isFullQuoted,
2120
getKey,
2221
removeQuotes,
2322
} from "WoltLabSuite/Core/Component/Quote/Storage";
2423
import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
2524
import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event";
2625

27-
interface Container {
26+
type Container = {
2827
element: HTMLElement;
2928
messageBodySelector: string;
3029
objectType: string;
31-
className: string;
3230
objectId: number;
33-
}
31+
/** @deprecated 6.2 Used for legacy implementations only. */
32+
className: string | undefined;
33+
};
3434

3535
let selectedMessage:
3636
| undefined
@@ -39,12 +39,12 @@ let selectedMessage:
3939
container: Container;
4040
};
4141

42-
interface ElementBoundaries {
42+
type ElementBoundaries = {
4343
bottom: number;
4444
left: number;
4545
right: number;
4646
top: number;
47-
}
47+
};
4848

4949
const containers = new Map<string, Container>();
5050
const quoteMessageButtons = new Map<string, HTMLElement>();
@@ -57,19 +57,19 @@ const copyQuote = document.createElement("div");
5757
export function registerContainer(
5858
containerSelector: string,
5959
messageBodySelector: string,
60-
className: string,
6160
objectType: string,
61+
className?: string,
6262
): void {
6363
wheneverFirstSeen(containerSelector, (container: HTMLElement) => {
6464
const id = DomUtil.identify(container);
65-
const objectId = ~~container.dataset.objectId!;
65+
const objectId = parseInt(container.dataset.objectId || "0");
6666

6767
containers.set(id, {
6868
element: container,
69-
messageBodySelector: messageBodySelector,
70-
objectType: objectType,
71-
className: className,
72-
objectId: objectId,
69+
messageBodySelector,
70+
objectType,
71+
objectId,
72+
className,
7373
});
7474

7575
if (container.classList.contains("jsInvalidQuoteTarget")) {
@@ -80,42 +80,53 @@ export function registerContainer(
8080
container.classList.add("jsQuoteMessageContainer");
8181

8282
const quoteMessage = container.querySelector<HTMLElement>(".jsQuoteMessage");
83-
let quoteMessageButton = quoteMessage?.querySelector<HTMLElement>(".button");
84-
if (!quoteMessageButton && quoteMessage?.classList.contains("button")) {
83+
if (quoteMessage === null) {
84+
return;
85+
}
86+
87+
let quoteMessageButton = quoteMessage.querySelector<HTMLElement>(".button");
88+
if (!quoteMessageButton && quoteMessage.classList.contains("button")) {
8589
quoteMessageButton = quoteMessage;
8690
}
8791

88-
if (quoteMessageButton) {
92+
if (quoteMessageButton !== null) {
8993
quoteMessageButtons.set(getKey(objectType, objectId), quoteMessageButton);
9094

91-
if (isFullQuoted(objectType, objectId)) {
95+
if (getFullQuoteUuid(objectType, objectId) !== undefined) {
9296
quoteMessageButton.classList.add("active");
9397
}
9498
}
9599

96-
quoteMessage?.addEventListener(
100+
quoteMessage.addEventListener(
97101
"click",
98102
promiseMutex(async (event: MouseEvent) => {
99103
event.preventDefault();
100104

101-
if (isFullQuoted(objectType, objectId)) {
102-
removeQuotes([getFullQuoteUuid(objectType, objectId)!]);
103-
quoteMessageButton!.classList.remove("active");
105+
const uuid = getFullQuoteUuid(objectType, objectId);
106+
if (uuid !== undefined) {
107+
removeQuotes([uuid]);
108+
quoteMessageButton?.classList.remove("active");
109+
104110
return;
105111
}
106112

107-
const quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!);
108-
quoteMessageButton!.classList.add("active");
113+
const quote = await saveFullQuote(objectType, objectId, className);
114+
quoteMessageButton?.classList.add("active");
109115

110116
if (activeEditor !== undefined) {
117+
const content = quote.rawMessage || quote.message;
118+
if (content === null) {
119+
throw new Error("Expected either the `rawMessage` or `message` to be a string.");
120+
}
121+
111122
dispatchToCkeditor(activeEditor.sourceElement).insertQuote({
112-
author: quoteMessage.author,
113-
content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage,
114-
isText: quoteMessage.rawMessage === undefined,
115-
link: quoteMessage.link,
123+
author: quote.author,
124+
content,
125+
isText: quote.rawMessage === null,
126+
link: quote.link,
116127
});
117128

118-
markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid);
129+
markQuoteAsUsed(activeEditor.sourceElement.id, quote.uuid);
119130
}
120131
}),
121132
);
@@ -152,17 +163,22 @@ function setup() {
152163
buttonSaveQuote.addEventListener(
153164
"click",
154165
promiseMutex(async () => {
166+
if (selectedMessage === undefined) {
167+
return;
168+
}
169+
155170
await saveQuote(
156-
selectedMessage!.container.objectType,
157-
selectedMessage!.container.objectId,
158-
selectedMessage!.container.className,
159-
selectedMessage!.message,
171+
selectedMessage.container.objectType,
172+
selectedMessage.container.objectId,
173+
selectedMessage.message,
174+
selectedMessage.container.className,
160175
);
161176

162177
removeSelection();
163178
}),
164179
);
165180
copyQuote.appendChild(buttonSaveQuote);
181+
166182
const buttonSaveAndInsertQuote = document.createElement("button");
167183
buttonSaveAndInsertQuote.type = "button";
168184
buttonSaveAndInsertQuote.hidden = true;
@@ -171,22 +187,31 @@ function setup() {
171187
buttonSaveAndInsertQuote.addEventListener(
172188
"click",
173189
promiseMutex(async () => {
174-
const quoteMessage = await saveQuote(
175-
selectedMessage!.container.objectType,
176-
selectedMessage!.container.objectId,
177-
selectedMessage!.container.className,
178-
selectedMessage!.message,
190+
if (selectedMessage === undefined) {
191+
return;
192+
}
193+
194+
const quote = await saveQuote(
195+
selectedMessage.container.objectType,
196+
selectedMessage.container.objectId,
197+
selectedMessage.message,
198+
selectedMessage.container.className,
179199
);
180200

181201
if (activeEditor !== undefined) {
202+
const content = quote.rawMessage || quote.message;
203+
if (content === null) {
204+
throw new Error("Expected either the `rawMessage` or `message` to be a string.");
205+
}
206+
182207
dispatchToCkeditor(activeEditor.sourceElement).insertQuote({
183-
author: quoteMessage.author,
184-
content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage,
185-
isText: quoteMessage.rawMessage === undefined,
186-
link: quoteMessage.link,
208+
author: quote.author,
209+
content,
210+
isText: quote.rawMessage === null,
211+
link: quote.link,
187212
});
188213

189-
markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid);
214+
markQuoteAsUsed(activeEditor.sourceElement.id, quote.uuid);
190215
}
191216

192217
removeSelection();

0 commit comments

Comments
 (0)