blob: a613fb796d11e135dca25655837d9ac992836eeb [file] [log] [blame]
Adrià Vilanova Martínez2d9be8d2022-12-28 00:50:14 +01001import GapModel from './Gap.js';
2import MessageModel from './Message.js';
3
Adrià Vilanova Martínez4b2582d2023-11-16 01:56:04 +01004// Keys of the PromotedMessages protobuf message which contain lists of promoted
5// messages.
6const kPromotedMessagesKeys = [1, 2, 3, 4, 5, 6];
7
Adrià Vilanova Martínez2d9be8d2022-12-28 00:50:14 +01008export default class ThreadModel {
Adrià Vilanova Martínez5dd7a6f2023-03-04 18:32:27 +01009 constructor(data) {
10 this.data = data ?? {};
11 }
12
13 getId() {
14 return this.data[2]?.[1]?.[1] ?? null;
15 }
16
17 getForumId() {
18 return this.data[2]?.[1]?.[3] ?? null;
19 }
20
21 getRawCommentsAndGaps() {
22 return this.data[40] ?? [];
23 }
24
25 setRawCommentsAndGaps(cogs) {
26 this.data[40] = cogs;
27 }
28
29 getMessageOrGapModels() {
30 const rawMogs = this.getRawCommentsAndGaps();
31 return rawMogs.filter(mog => mog !== undefined).map(mog => {
32 if (mog[1]) return new MessageModel(mog[1], this);
33 if (mog[2]) return new GapModel(mog[2], this);
34 });
35 }
36
37 setLastMessage(message) {
38 if (!this.data[17]) this.data[17] = [];
39 this.data[17][3] = message;
40 }
41
42 setNumMessages(num) {
43 this.data[8] = num;
44 }
45
46 isLocked() {
47 // TODO: When a forum is read-only, this should also return true.
48 return this.data[2]?.[5] == true;
49 }
50
51 isSoftLocked() {
52 return this.data[2]?.[51] == true;
53 }
54
55 isAuthoredByUser() {
56 return this.data[9] == true;
57 }
58
59 toRawThread() {
60 return this.data;
61 }
62
Adrià Vilanova Martínez4b2582d2023-11-16 01:56:04 +010063 getPromotedMessagesList() {
64 const promotedMessages = [];
65 for (const key of kPromotedMessagesKeys) {
66 const messagesList = this.data[17][key] ?? [];
67 for (const rawMessage of messagesList) {
68 const message = new MessageModel(rawMessage);
69 if (message.getId() === null) continue;
70
71 const isMessageAlreadyIncluded = promotedMessages.some(
72 existingMessage => existingMessage.getId() == message.getId());
73 if (isMessageAlreadyIncluded) continue;
74
75 promotedMessages.push(message);
76 }
77 }
78 return promotedMessages;
79 }
80
81 /**
82 * Get a list with all the messages contained in the model.
83 */
84 getAllMessagesList() {
85 const messages = [];
86
87 for (const messageOrGap of this.getMessageOrGapModels()) {
88 if (!(messageOrGap instanceof MessageModel)) continue;
89 messages.push(messageOrGap);
90 for (const subMessageOrGap of messageOrGap.getCommentsAndGaps()) {
91 if (!(subMessageOrGap instanceof MessageModel)) continue;
92 messages.push(subMessageOrGap);
93 }
94 }
95
96 const promotedMessages = this.getPromotedMessagesList();
97 for (const message of promotedMessages) {
98 const isMessageAlreadyIncluded = messages.some(
99 existingMessage => existingMessage.getId() == message.getId());
100 if (isMessageAlreadyIncluded) continue;
101
102 messages.push(message);
103 }
104
105 return messages;
106 }
107
Adrià Vilanova Martínez2d9be8d2022-12-28 00:50:14 +0100108 /**
109 * The following code is based on logic written by Googlers in the TW frontend
110 * and thus is not included as part of the MIT license.
111 */
112 static mergeMessageOrGaps(a, b) {
113 if (a.length == 0 || b.length == 0)
114 return a.length > 0 ? a : b.length > 0 ? b : [];
115
116 let e = [];
117 for (let g = 0, k = 0, m = 0, q = a[g], u = b[k];
118 g < a.length && k < b.length;) {
119 if (q instanceof MessageModel && u instanceof MessageModel) {
120 if (q.getCreatedMicroseconds() === u.getCreatedMicroseconds()) {
121 u.mergeCommentOrGapViews(q);
122 }
123
124 e.push(u);
125
126 if (g === a.length - 1 || k === b.length - 1) {
127 for (; ++g < a.length;) e.push(a[g]);
128 for (; ++k < b.length;) e.push(b[k]);
129 break;
130 }
131
132 q = a[++g];
133 u = b[++k];
134 } else {
135 if (u instanceof GapModel) {
136 let z;
137 for (z = q instanceof MessageModel ? q.getCreatedMicroseconds() :
138 q.getEndTimestamp();
139 z < u.getEndTimestamp();) {
140 e.push(q);
141 m += q instanceof GapModel ? q.getCount() : 1;
142 if (g === a.length - 1) break;
143 q = a[++g];
144 z = q instanceof MessageModel ? q.getCreatedMicroseconds() :
145 q.getEndTimestamp();
146 }
147 if (q instanceof GapModel && u.getCount() - m > 0 &&
148 z >= u.getEndTimestamp()) {
149 const gm = new GapModel();
150 gm.setCount(u.getCount() - m);
151 gm.setStartMicroseconds('' + q.getStartTimestamp());
152 gm.setEndMicroseconds('' + u.getEndTimestamp());
153 gm.setParentId(u.getParentId());
154 e.push(gm);
155 m = u.getCount() - m;
156 } else {
157 m = 0;
158 }
159 if (k === b.length - 1) break;
160 u = b[++k];
161 }
162 if (q instanceof GapModel) {
163 let z;
164 for (z = u instanceof MessageModel ? u.getCreatedMicroseconds() :
165 u.getEndTimestamp();
166 z < q.getEndTimestamp();) {
167 e.push(u);
168 m += u instanceof GapModel ? u.getCount() : 1;
169 if (k === b.length - 1) break;
170 u = b[++k];
171 z = u instanceof MessageModel ? u.getCreatedMicroseconds() :
172 u.getEndTimestamp();
173 }
174 if (u instanceof GapModel && q.getCount() - m > 0 &&
175 z >= q.getEndTimestamp()) {
176 const gm = new GapModel();
177 gm.setCount(q.getCount() - m);
178 gm.setStartMicroseconds('' + u.getStartTimestamp());
179 gm.setEndMicroseconds('' + q.getEndTimestamp());
180 gm.setParentId(q.getParentId());
181 e.push(gm);
182 m = q.getCount() - m;
183 } else {
184 m = 0;
185 }
186 if (g === a.length - 1) break;
187 q = a[++g];
188 }
189 }
190 }
191 return e;
192 }
193
194 static mergeMessageOrGapsMultiarray(mogsModels) {
195 if (mogsModels.length < 1) return [];
196 let mergeResult = mogsModels[0];
197 for (let i = 1; i < mogsModels.length; ++i) {
198 mergeResult = ThreadModel.mergeMessageOrGaps(mergeResult, mogsModels[i]);
199 }
200 return mergeResult;
201 }
202}