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/models/Message.js b/src/models/Message.js
new file mode 100644
index 0000000..d39a9ab
--- /dev/null
+++ b/src/models/Message.js
@@ -0,0 +1,52 @@
+import GapModel from './Gap.js';
+import ThreadModel from './Thread.js';
+
+export default class MessageModel {
+  constructor(data) {
+    this.data = data ?? {};
+    this.commentsAndGaps = null;
+  }
+
+  getCreatedTimestamp() {
+    return this.data[1]?.[1]?.[2] ?? null;
+  }
+
+  getCreatedMicroseconds() {
+    const a = this.getCreatedTimestamp();
+    if (a === null) a = '0';
+    return BigInt(a);
+  }
+
+  getRawCommentsAndGaps() {
+    return this.data[12] ?? [];
+  }
+
+  getCommentsAndGaps() {
+    if (this.commentsAndGaps === null)
+      this.commentsAndGaps =
+          MessageModel.mapToMessageOrGapModels(this.getRawCommentsAndGaps());
+    return this.commentsAndGaps;
+  }
+
+  clearCommentsAndGaps() {
+    this.commentsAndGaps = [];
+    this.data[12] = [];
+  }
+
+  toRawMessageOrGap() {
+    return {1: this.data};
+  }
+
+  static mapToMessageOrGapModels(rawArray) {
+    return rawArray.map(mog => {
+      if (mog[1]) return new MessageModel(mog[1]);
+      if (mog[2]) return new GapModel(mog[2]);
+    });
+  }
+
+  mergeCommentOrGapViews(a) {
+    this.commentsAndGaps = ThreadModel.mergeMessageOrGaps(
+        a.getCommentsAndGaps(), this.getCommentsAndGaps());
+    this.data[12] = this.commentsAndGaps.map(cog => cog.toRawMessageOrGap());
+  }
+}