refactor(response-modifier): rewrite modifiers with Typescript
Bug: twpowertools:230
Change-Id: Ibedccb24445098aae87fdbca94b0868bdcfccd41
diff --git a/src/xhrInterceptor/responseModifiers/createMessageRemoveParentRef.js b/src/xhrInterceptor/responseModifiers/createMessageRemoveParentRef.ts
similarity index 76%
rename from src/xhrInterceptor/responseModifiers/createMessageRemoveParentRef.js
rename to src/xhrInterceptor/responseModifiers/createMessageRemoveParentRef.ts
index a127975..5fa59b5 100644
--- a/src/xhrInterceptor/responseModifiers/createMessageRemoveParentRef.js
+++ b/src/xhrInterceptor/responseModifiers/createMessageRemoveParentRef.ts
@@ -1,4 +1,6 @@
-const createMessageRemoveParentRef = {
+import { Modifier } from "./types";
+
+const createMessageRemoveParentRef: Modifier = {
urlRegex: /api\/CreateMessage/i,
featureGated: true,
features: ['flattenthreads', 'flattenthreads_switch_enabled'],
@@ -6,7 +8,7 @@
return options['flattenthreads'] &&
options['flattenthreads_switch_enabled'];
},
- async interceptor(_request, response) {
+ async interceptor(response) {
// Remove parent_message_id value (field 37)
delete response[37];
return response;
diff --git a/src/xhrInterceptor/responseModifiers/demo.js b/src/xhrInterceptor/responseModifiers/demo.js
deleted file mode 100644
index 5839207..0000000
--- a/src/xhrInterceptor/responseModifiers/demo.js
+++ /dev/null
@@ -1,18 +0,0 @@
-export default {
- urlRegex: /api\/ViewForum/i,
- featureGated: true,
- features: ['demo1', 'demo2'],
- isEnabled(features) {
- return features['demo1'] || features['demo2'];
- },
- interceptor(_request, _response) {
- return Promise.resolve({
- 1: {
- 2: [],
- 4: 0,
- 6: {},
- 7: {},
- },
- });
- },
-};
diff --git a/src/xhrInterceptor/responseModifiers/flattenThread.js b/src/xhrInterceptor/responseModifiers/flattenThread.js
deleted file mode 100644
index 6923178..0000000
--- a/src/xhrInterceptor/responseModifiers/flattenThread.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import {kAdditionalInfoClass} from '../../features/flattenThreads/core/flattenThreads.js';
-import GapModel from '../../models/Gap.js';
-import MessageModel from '../../models/Message';
-import StartupDataModel from '../../models/StartupData.js';
-import ThreadModel from '../../models/Thread';
-
-const currentUser = StartupDataModel.buildFromCCDOM().getCurrentUserModel();
-
-const flattenThread = {
- urlRegex: /api\/ViewThread/i,
- featureGated: true,
- features: ['flattenthreads', 'flattenthreads_switch_enabled'],
- isEnabled(options) {
- return options['flattenthreads'] &&
- options['flattenthreads_switch_enabled'];
- },
- async interceptor(_request, response) {
- if (!response[1]?.[40]) return response;
-
- const thread = new ThreadModel(response[1]);
-
- // Do the actual flattening
- const originalMogs = thread.getMessageOrGapModels();
- let extraMogs = [];
- originalMogs.forEach(mog => {
- if (mog instanceof GapModel) return;
- const cogs = mog.getCommentsAndGaps();
- extraMogs = extraMogs.concat(cogs);
- mog.clearCommentsAndGaps();
- });
- const mogs = originalMogs.concat(extraMogs);
-
- // Add some message data to the payload so the extension can show the parent
- // comment/reply in the case of comments.
- let prevReplyId;
- let prevReplyParentId;
- mogs.forEach(m => {
- const info = this.getAdditionalInformation(
- m, mogs, prevReplyId, prevReplyParentId);
- prevReplyId = m.getId();
- prevReplyParentId = info.parentId;
-
- const extraInfoEl = document.createElement('code');
- extraInfoEl.textContent = JSON.stringify(info);
- extraInfoEl.setAttribute('style', 'display: none');
- extraInfoEl.classList.add(kAdditionalInfoClass);
- m.newPayload = m.getPayload() + extraInfoEl.outerHTML;
- });
- mogs.forEach(m => m.setPayload(m.newPayload));
-
- // Clear parent_message_id fields
- mogs.forEach(m => m.clearParentMessageId());
-
- // Sort the messages by date
- mogs.sort((a, b) => {
- const c = a instanceof MessageModel ? a.getCreatedMicroseconds() :
- a.getStartTimestamp();
- const d = b instanceof MessageModel ? b.getCreatedMicroseconds() :
- b.getStartTimestamp();
- const diff = c - d;
- return diff > 0 ? 1 : diff < 0 ? -1 : 0;
- });
-
- thread.setRawCommentsAndGaps(mogs.map(mog => mog.toRawMessageOrGap()));
-
- // Set last_message to the last message after sorting
- thread.setLastMessage(thread.getRawCommentsAndGaps().slice(-1)?.[1]);
-
- // Set num_messages to the updated value, since we've flattened the replies.
- thread.setNumMessages(thread.getRawCommentsAndGaps().length);
-
- response[1] = thread.toRawThread();
- return response;
- },
- getAdditionalInformation(message, mogs, prevReplyId, prevReplyParentId) {
- const id = message.getId();
- const parentId = message.getParentMessageId();
- const authorName = message.getAuthor()?.[1]?.[1];
- const canComment = message.canComment(currentUser);
- if (!parentId) {
- return {
- isComment: false,
- id,
- authorName,
- canComment,
- };
- }
-
- let prevId;
- if (parentId == prevReplyParentId && prevReplyParentId)
- prevId = prevReplyId;
- else
- prevId = parentId;
-
- const prevMessage = prevId ? mogs.find(m => m.getId() == prevId) : null;
-
- return {
- isComment: true,
- id,
- authorName,
- parentId,
- prevMessage: {
- id: prevId,
- payload: prevMessage?.getPayload(),
- author: prevMessage?.getAuthor(),
- },
- canComment,
- };
- },
-};
-
-export default flattenThread;
diff --git a/src/xhrInterceptor/responseModifiers/flattenThread.ts b/src/xhrInterceptor/responseModifiers/flattenThread.ts
new file mode 100644
index 0000000..2cd1771
--- /dev/null
+++ b/src/xhrInterceptor/responseModifiers/flattenThread.ts
@@ -0,0 +1,163 @@
+import { ProtobufNumber, ProtobufObject } from '../../common/protojs.types';
+import { kAdditionalInfoClass } from '../../features/flattenThreads/core/flattenThreads.js';
+import GapModel from '../../models/Gap';
+import MessageModel from '../../models/Message';
+import StartupDataModel from '../../models/StartupData';
+import ThreadModel from '../../models/Thread';
+import { Modifier } from './types';
+
+const currentUser = StartupDataModel.buildFromCCDOM().getCurrentUserModel();
+
+const flattenThread: Modifier = {
+ urlRegex: /api\/ViewThread/i,
+ featureGated: true,
+ features: ['flattenthreads', 'flattenthreads_switch_enabled'],
+ isEnabled(options) {
+ return (
+ options['flattenthreads'] && options['flattenthreads_switch_enabled']
+ );
+ },
+ async interceptor(response) {
+ if (!response[1]?.[40]) return response;
+
+ const thread = new ThreadModel(response[1]);
+
+ // Do the actual flattening
+ const originalMogs = thread.getMessageOrGapModels();
+ let extraMogs: Array<MessageModel | GapModel> = [];
+ originalMogs.forEach((mog) => {
+ if (mog instanceof GapModel) return;
+ const cogs = mog.getCommentsAndGaps();
+ extraMogs = extraMogs.concat(cogs);
+ mog.clearCommentsAndGaps();
+ });
+ const mogs = originalMogs.concat(extraMogs);
+
+ // Add some message data to the payload so the extension can show the parent
+ // comment/reply in the case of comments.
+ const newPayloads: Record<string, string> = {};
+ let prevReplyId: ProtobufNumber | undefined = undefined;
+ let prevReplyParentId: ProtobufNumber | undefined = undefined;
+ mogs.forEach((m) => {
+ if (m instanceof GapModel) return;
+
+ const info = getAdditionalInformation(
+ m,
+ mogs,
+ prevReplyId,
+ prevReplyParentId,
+ );
+ prevReplyId = m.getId();
+ prevReplyParentId = info.isComment ? info.parentId : undefined;
+
+ const extraInfoEl = document.createElement('code');
+ extraInfoEl.textContent = JSON.stringify(info);
+ extraInfoEl.setAttribute('style', 'display: none');
+ extraInfoEl.classList.add(kAdditionalInfoClass);
+ newPayloads[m.getId()] = m.getPayload() + extraInfoEl.outerHTML;
+ });
+ mogs.forEach(
+ (m) => m instanceof MessageModel && m.setPayload(newPayloads[m.getId()]),
+ );
+
+ // Clear parent_message_id fields
+ mogs.forEach((m) => m instanceof MessageModel && m.clearParentMessageId());
+
+ // Sort the messages by date
+ mogs.sort((a, b) => {
+ const c =
+ a instanceof MessageModel
+ ? a.getCreatedMicroseconds()
+ : a.getStartTimestamp();
+ const d =
+ b instanceof MessageModel
+ ? b.getCreatedMicroseconds()
+ : b.getStartTimestamp();
+ const diff = c - d;
+ return diff > 0 ? 1 : diff < 0 ? -1 : 0;
+ });
+
+ thread.setRawCommentsAndGaps(mogs.map((mog) => mog.toRawMessageOrGap()));
+
+ // Set last_message to the last message after sorting
+ thread.setLastMessage(thread.getRawCommentsAndGaps().slice(-1)?.[1]);
+
+ // Set num_messages to the updated value, since we've flattened the replies.
+ thread.setNumMessages(thread.getRawCommentsAndGaps().length);
+
+ response[1] = thread.toRawThread();
+ return response;
+ },
+};
+
+interface BaseAdditionalInformation {
+ id: ProtobufNumber;
+ authorName: string;
+ canComment: boolean;
+}
+
+type CommentAdditionalInformation =
+ | {
+ isComment: false;
+ }
+ | {
+ isComment: true;
+ parentId: ProtobufNumber;
+ prevMessage: {
+ id: ProtobufNumber;
+ payload: string;
+ author: ProtobufObject | null;
+ };
+ };
+
+export type AdditionalInformation = BaseAdditionalInformation &
+ CommentAdditionalInformation;
+
+function getAdditionalInformation(
+ message: MessageModel,
+ mogs: Array<MessageModel | GapModel>,
+ prevReplyId: ProtobufNumber | undefined,
+ prevReplyParentId: ProtobufNumber | undefined,
+): AdditionalInformation {
+ const id = message.getId();
+ const parentId = message.getParentMessageId();
+ const authorName = message.getAuthor()?.[1]?.[1];
+ const canComment = message.canComment(currentUser);
+ if (!parentId) {
+ return {
+ isComment: false,
+ id,
+ authorName,
+ canComment,
+ };
+ }
+
+ let prevId;
+ if (parentId == prevReplyParentId && prevReplyParentId) {
+ prevId = prevReplyId;
+ } else {
+ prevId = parentId;
+ }
+
+ const prevMessage = prevId
+ ? mogs.find(
+ (m): m is MessageModel =>
+ m instanceof MessageModel && m.getId() == prevId,
+ )
+ : null;
+
+ return {
+ isComment: true,
+ id,
+ authorName,
+ parentId,
+ prevMessage: {
+ id: prevId,
+ payload: prevMessage?.getPayload(),
+ author: prevMessage?.getAuthor(),
+ },
+ canComment,
+ };
+}
+
+export default flattenThread;
diff --git a/src/xhrInterceptor/responseModifiers/index.js b/src/xhrInterceptor/responseModifiers/index.js
index 057960f..b822153 100644
--- a/src/xhrInterceptor/responseModifiers/index.js
+++ b/src/xhrInterceptor/responseModifiers/index.js
@@ -1,9 +1,9 @@
import MWOptionsWatcherClient from '../../common/mainWorldOptionsWatcher/Client.js';
import {convertJSONToResponse, convertJSONToResponseText, getResponseJSON} from '../utils.js';
-import createMessageRemoveParentRef from './createMessageRemoveParentRef.js';
-import flattenThread from './flattenThread.js';
-import loadMoreThread from './loadMoreThread.js';
+import createMessageRemoveParentRef from './createMessageRemoveParentRef';
+import flattenThread from './flattenThread';
+import loadMoreThread from './loadMoreThread';
export const responseModifiers = [
loadMoreThread,
diff --git a/src/xhrInterceptor/responseModifiers/loadMoreThread.js b/src/xhrInterceptor/responseModifiers/loadMoreThread.js
deleted file mode 100644
index 2b2fe63..0000000
--- a/src/xhrInterceptor/responseModifiers/loadMoreThread.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import {CCApi} from '../../common/api.js';
-import {getAuthUser} from '../../common/communityConsoleUtils.js';
-import GapModel from '../../models/Gap.js';
-import MessageModel from '../../models/Message';
-import ThreadModel from '../../models/Thread';
-
-const authuser = getAuthUser();
-
-const loadMoreThread = {
- urlRegex: /api\/ViewThread/i,
- featureGated: true,
- features: ['flattenthreads', 'flattenthreads_switch_enabled'],
- isEnabled(options) {
- return options['flattenthreads'] &&
- options['flattenthreads_switch_enabled'];
- },
- async interceptor(request, response) {
- if (!response[1]?.[40]) return response;
-
- const thread = new ThreadModel(response[1]);
-
- if (!thread.getForumId() || !thread.getId()) {
- console.error(
- '[loadMoreThread] Couldn\'t find forum id and thread id for:',
- request.$TWPTRequestURL);
- return response;
- }
-
- const mogs = thread.getMessageOrGapModels();
- thread.setRawCommentsAndGaps(
- await this.loadGaps(thread.getForumId(), thread.getId(), mogs, 0));
-
- response[1] = thread.toRawThread();
- return response;
- },
- loadGaps(forumId, threadId, mogs, it) {
- if (it >= 10) {
- return Promise.reject(new Error(
- 'loadGaps has been called for more than 10 times, ' +
- 'which means we\'ve entered an infinite loop.'));
- }
-
- const messageOrGapPromises = [];
- messageOrGapPromises.push(
- Promise.resolve(mogs.filter(mog => mog !== undefined)));
- mogs.forEach(mog => {
- if (mog instanceof GapModel) {
- messageOrGapPromises.push(this.loadGap(forumId, threadId, mog));
- }
- if (mog instanceof MessageModel) {
- mog.getCommentsAndGaps().forEach(cog => {
- if (cog instanceof GapModel) {
- messageOrGapPromises.push(this.loadGap(forumId, threadId, cog));
- }
- });
- }
- });
-
- return Promise.all(messageOrGapPromises).then(res => {
- // #!if !production
- console.time('mergeMessages');
- // #!endif
- const mogs = ThreadModel.mergeMessageOrGapsMultiarray(res);
- // #!if !production
- console.timeEnd('mergeMessages');
- // #!endif
-
- if (mogs.some(mog => {
- return mog instanceof GapModel ||
- mog.getCommentsAndGaps().some(cog => cog instanceof GapModel);
- })) {
- return this.loadGaps(forumId, threadId, mogs, it + 1);
- }
- return mogs.map(message => message.toRawMessageOrGap());
- });
- },
- loadGap(forumId, threadId, gap) {
- return CCApi(
- 'ViewThread', {
- 1: forumId,
- 2: threadId,
- 3: {
- // options
- 1: {
- // pagination
- 2: gap.getCount(), // maxNum
- 7: {
- // targetRange
- 1: gap.getStartMicroseconds(), // startMicroseconds
- 2: gap.getEndMicroseconds(), // endMicroseconds
- 3: gap.getParentId(), // parentId
- },
- },
- 5: true, // withUserProfile
- 10: true, // withPromotedMessages
- },
- },
- /* authenticated = */ true, authuser)
- .then(res => {
- const thread = new ThreadModel(res[1]);
- return thread.getMessageOrGapModels();
- });
- }
-};
-
-export default loadMoreThread;
diff --git a/src/xhrInterceptor/responseModifiers/loadMoreThread.ts b/src/xhrInterceptor/responseModifiers/loadMoreThread.ts
new file mode 100644
index 0000000..06613d5
--- /dev/null
+++ b/src/xhrInterceptor/responseModifiers/loadMoreThread.ts
@@ -0,0 +1,132 @@
+import { CCApi } from '../../common/api.js';
+import { getAuthUser } from '../../common/communityConsoleUtils.js';
+import { ProtobufNumber } from '../../common/protojs.types.js';
+import GapModel from '../../models/Gap.js';
+import MessageModel from '../../models/Message';
+import ThreadModel from '../../models/Thread';
+import { Modifier } from './types.js';
+
+const authuser = getAuthUser();
+
+const loadMoreThread: Modifier = {
+ urlRegex: /api\/ViewThread/i,
+ featureGated: true,
+ features: ['flattenthreads', 'flattenthreads_switch_enabled'],
+ isEnabled(options) {
+ return (
+ options['flattenthreads'] && options['flattenthreads_switch_enabled']
+ );
+ },
+ async interceptor(response, url) {
+ if (!response[1]?.[40]) return response;
+
+ const thread = new ThreadModel(response[1]);
+
+ if (!thread.getForumId() || !thread.getId()) {
+ console.error(
+ "[loadMoreThread] Couldn't find forum id and thread id for:",
+ url,
+ );
+ return response;
+ }
+
+ const mogs = thread.getMessageOrGapModels();
+ thread.setRawCommentsAndGaps(
+ await loadGaps(thread.getForumId(), thread.getId(), mogs, 0),
+ );
+
+ response[1] = thread.toRawThread();
+ return response;
+ },
+};
+
+function loadGaps(
+ forumId: ProtobufNumber,
+ threadId: ProtobufNumber,
+ mogs: Array<MessageModel | GapModel>,
+ it: number,
+): Promise<Array<MessageModel | GapModel>> {
+ if (it >= 10) {
+ return Promise.reject(
+ new Error(
+ 'loadGaps has been called for more than 10 times, ' +
+ "which means we've entered an infinite loop.",
+ ),
+ );
+ }
+
+ const messageOrGapPromises = [];
+ messageOrGapPromises.push(
+ Promise.resolve(mogs.filter((mog) => mog !== undefined)),
+ );
+ mogs.forEach((mog) => {
+ if (mog instanceof GapModel) {
+ messageOrGapPromises.push(loadGap(forumId, threadId, mog));
+ }
+ if (mog instanceof MessageModel) {
+ mog.getCommentsAndGaps().forEach((cog) => {
+ if (cog instanceof GapModel) {
+ messageOrGapPromises.push(loadGap(forumId, threadId, cog));
+ }
+ });
+ }
+ });
+
+ return Promise.all(messageOrGapPromises).then((res) => {
+ // #!if !production
+ console.time('mergeMessages');
+ // #!endif
+ const mogs = ThreadModel.mergeMessageOrGapsMultiarray(res);
+ // #!if !production
+ console.timeEnd('mergeMessages');
+ // #!endif
+
+ if (
+ mogs.some((mog) => {
+ return (
+ mog instanceof GapModel ||
+ mog.getCommentsAndGaps().some((cog) => cog instanceof GapModel)
+ );
+ })
+ ) {
+ return loadGaps(forumId, threadId, mogs, it + 1);
+ }
+ return mogs.map((message) => message.toRawMessageOrGap());
+ });
+}
+
+async function loadGap(
+ forumId: ProtobufNumber,
+ threadId: ProtobufNumber,
+ gap: GapModel,
+): Promise<Array<MessageModel | GapModel>> {
+ return CCApi(
+ 'ViewThread',
+ {
+ 1: forumId,
+ 2: threadId,
+ 3: {
+ // options
+ 1: {
+ // pagination
+ 2: gap.getCount(), // maxNum
+ 7: {
+ // targetRange
+ 1: gap.getStartMicroseconds(), // startMicroseconds
+ 2: gap.getEndMicroseconds(), // endMicroseconds
+ 3: gap.getParentId(), // parentId
+ },
+ },
+ 5: true, // withUserProfile
+ 10: true, // withPromotedMessages
+ },
+ },
+ /* authenticated = */ true,
+ authuser,
+ ).then((res) => {
+ const thread = new ThreadModel(res[1]);
+ return thread.getMessageOrGapModels();
+ });
+}
+
+export default loadMoreThread;
diff --git a/src/xhrInterceptor/responseModifiers/types.ts b/src/xhrInterceptor/responseModifiers/types.ts
new file mode 100644
index 0000000..d67b9e3
--- /dev/null
+++ b/src/xhrInterceptor/responseModifiers/types.ts
@@ -0,0 +1,13 @@
+import {
+ OptionCodename,
+ OptionsValues,
+} from '../../common/options/optionsPrototype';
+import { ProtobufObject } from '../../common/protojs.types';
+
+export interface Modifier {
+ urlRegex: RegExp;
+ featureGated: Boolean;
+ features: OptionCodename[];
+ isEnabled(options: OptionsValues): Boolean;
+ interceptor(response: ProtobufObject, url: string): Promise<ProtobufObject>;
+}