fix(extra-info): handle successive message loads and promoted messages

When loading a thread, some messages might be collapsed and are loaded
in subsequent requests. This CL handles this case by holding an array of
messages which grows each time the user expands a gap of messages.

Furthermore, this CL also considers promoted messages for the message
list (before this, the extra info chips were added to promoted messages
only if they were loaded outside of the promoted messages structure as
well).

Bug: twpowertools:93
Change-Id: I8b6f4e8f4a97c7f5e4cdde52b6b773b9631fbe57
diff --git a/src/models/Thread.js b/src/models/Thread.js
index e87f541..a613fb7 100644
--- a/src/models/Thread.js
+++ b/src/models/Thread.js
@@ -1,6 +1,10 @@
 import GapModel from './Gap.js';
 import MessageModel from './Message.js';
 
+// Keys of the PromotedMessages protobuf message which contain lists of promoted
+// messages.
+const kPromotedMessagesKeys = [1, 2, 3, 4, 5, 6];
+
 export default class ThreadModel {
   constructor(data) {
     this.data = data ?? {};
@@ -56,6 +60,51 @@
     return this.data;
   }
 
+  getPromotedMessagesList() {
+    const promotedMessages = [];
+    for (const key of kPromotedMessagesKeys) {
+      const messagesList = this.data[17][key] ?? [];
+      for (const rawMessage of messagesList) {
+        const message = new MessageModel(rawMessage);
+        if (message.getId() === null) continue;
+
+        const isMessageAlreadyIncluded = promotedMessages.some(
+            existingMessage => existingMessage.getId() == message.getId());
+        if (isMessageAlreadyIncluded) continue;
+
+        promotedMessages.push(message);
+      }
+    }
+    return promotedMessages;
+  }
+
+  /**
+   * Get a list with all the messages contained in the model.
+   */
+  getAllMessagesList() {
+    const messages = [];
+
+    for (const messageOrGap of this.getMessageOrGapModels()) {
+      if (!(messageOrGap instanceof MessageModel)) continue;
+      messages.push(messageOrGap);
+      for (const subMessageOrGap of messageOrGap.getCommentsAndGaps()) {
+        if (!(subMessageOrGap instanceof MessageModel)) continue;
+        messages.push(subMessageOrGap);
+      }
+    }
+
+    const promotedMessages = this.getPromotedMessagesList();
+    for (const message of promotedMessages) {
+      const isMessageAlreadyIncluded = messages.some(
+          existingMessage => existingMessage.getId() == message.getId());
+      if (isMessageAlreadyIncluded) continue;
+
+      messages.push(message);
+    }
+
+    return messages;
+  }
+
   /**
    * The following code is based on logic written by Googlers in the TW frontend
    * and thus is not included as part of the MIT license.