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