refactor(response-modifier): rewrite modifiers with Typescript

Bug: twpowertools:230
Change-Id: Ibedccb24445098aae87fdbca94b0868bdcfccd41
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;