| 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 ?? {}; |
| } |
| |
| getId() { |
| return this.data[2]?.[1]?.[1] ?? null; |
| } |
| |
| getForumId() { |
| return this.data[2]?.[1]?.[3] ?? null; |
| } |
| |
| getRawCommentsAndGaps() { |
| return this.data[40] ?? []; |
| } |
| |
| setRawCommentsAndGaps(cogs) { |
| this.data[40] = cogs; |
| } |
| |
| getMessageOrGapModels() { |
| const rawMogs = this.getRawCommentsAndGaps(); |
| return rawMogs.filter(mog => mog !== undefined).map(mog => { |
| if (mog[1]) return new MessageModel(mog[1], this); |
| if (mog[2]) return new GapModel(mog[2], this); |
| }); |
| } |
| |
| setLastMessage(message) { |
| if (!this.data[17]) this.data[17] = []; |
| this.data[17][3] = message; |
| } |
| |
| setNumMessages(num) { |
| this.data[8] = num; |
| } |
| |
| isLocked() { |
| // TODO: When a forum is read-only, this should also return true. |
| return this.data[2]?.[5] == true; |
| } |
| |
| isSoftLocked() { |
| return this.data[2]?.[51] == true; |
| } |
| |
| isAuthoredByUser() { |
| return this.data[9] == true; |
| } |
| |
| toRawThread() { |
| 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. |
| */ |
| static mergeMessageOrGaps(a, b) { |
| if (a.length == 0 || b.length == 0) |
| return a.length > 0 ? a : b.length > 0 ? b : []; |
| |
| let e = []; |
| for (let g = 0, k = 0, m = 0, q = a[g], u = b[k]; |
| g < a.length && k < b.length;) { |
| if (q instanceof MessageModel && u instanceof MessageModel) { |
| if (q.getCreatedMicroseconds() === u.getCreatedMicroseconds()) { |
| u.mergeCommentOrGapViews(q); |
| } |
| |
| e.push(u); |
| |
| if (g === a.length - 1 || k === b.length - 1) { |
| for (; ++g < a.length;) e.push(a[g]); |
| for (; ++k < b.length;) e.push(b[k]); |
| break; |
| } |
| |
| q = a[++g]; |
| u = b[++k]; |
| } else { |
| if (u instanceof GapModel) { |
| let z; |
| for (z = q instanceof MessageModel ? q.getCreatedMicroseconds() : |
| q.getEndTimestamp(); |
| z < u.getEndTimestamp();) { |
| e.push(q); |
| m += q instanceof GapModel ? q.getCount() : 1; |
| if (g === a.length - 1) break; |
| q = a[++g]; |
| z = q instanceof MessageModel ? q.getCreatedMicroseconds() : |
| q.getEndTimestamp(); |
| } |
| if (q instanceof GapModel && u.getCount() - m > 0 && |
| z >= u.getEndTimestamp()) { |
| const gm = new GapModel(); |
| gm.setCount(u.getCount() - m); |
| gm.setStartMicroseconds('' + q.getStartTimestamp()); |
| gm.setEndMicroseconds('' + u.getEndTimestamp()); |
| gm.setParentId(u.getParentId()); |
| e.push(gm); |
| m = u.getCount() - m; |
| } else { |
| m = 0; |
| } |
| if (k === b.length - 1) break; |
| u = b[++k]; |
| } |
| if (q instanceof GapModel) { |
| let z; |
| for (z = u instanceof MessageModel ? u.getCreatedMicroseconds() : |
| u.getEndTimestamp(); |
| z < q.getEndTimestamp();) { |
| e.push(u); |
| m += u instanceof GapModel ? u.getCount() : 1; |
| if (k === b.length - 1) break; |
| u = b[++k]; |
| z = u instanceof MessageModel ? u.getCreatedMicroseconds() : |
| u.getEndTimestamp(); |
| } |
| if (u instanceof GapModel && q.getCount() - m > 0 && |
| z >= q.getEndTimestamp()) { |
| const gm = new GapModel(); |
| gm.setCount(q.getCount() - m); |
| gm.setStartMicroseconds('' + u.getStartTimestamp()); |
| gm.setEndMicroseconds('' + q.getEndTimestamp()); |
| gm.setParentId(q.getParentId()); |
| e.push(gm); |
| m = q.getCount() - m; |
| } else { |
| m = 0; |
| } |
| if (g === a.length - 1) break; |
| q = a[++g]; |
| } |
| } |
| } |
| return e; |
| } |
| |
| static mergeMessageOrGapsMultiarray(mogsModels) { |
| if (mogsModels.length < 1) return []; |
| let mergeResult = mogsModels[0]; |
| for (let i = 1; i < mogsModels.length; ++i) { |
| mergeResult = ThreadModel.mergeMessageOrGaps(mergeResult, mogsModels[i]); |
| } |
| return mergeResult; |
| } |
| } |