Add flattenthreads experiment

This experiment allows users to flatten the replies in threads, so they
are shown linearly in a chronological way instead of nested.

When the option is enabled, a switch is added to the thread page which
lets the user switch between flattening replies and not flattening them.

Some UI is still missing (see the design document[1]).

[1]: https://docs.google.com/document/d/1P-HanTHxaOFF_FHh0uSv0GIhG1dxWTJTGoT6VPjjvY0/edit

Bug: twpowertools:153
Change-Id: I43f94442cadc12b752700f0e8d974522be621d3e
diff --git a/src/xhrInterceptor/responseModifiers/flattenThread.js b/src/xhrInterceptor/responseModifiers/flattenThread.js
new file mode 100644
index 0000000..65eb42c
--- /dev/null
+++ b/src/xhrInterceptor/responseModifiers/flattenThread.js
@@ -0,0 +1,40 @@
+import GapModel from '../../models/Gap.js';
+import MessageModel from '../../models/Message.js';
+
+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 originalMogs =
+        MessageModel.mapToMessageOrGapModels(response[1][40] ?? []);
+    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);
+    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;
+    });
+    response[1][40] = mogs.map(mog => mog.toRawMessageOrGap());
+    // Set num_messages to the updated value, since we've flattened the replies.
+    response[1][8] = response[1][40].length;
+    return response;
+  },
+};
+
+export default loadMoreThread;