| import { ProtobufNumber, ProtobufObject } from '../common/protojs.types.js'; |
| import GapModel from './Gap.js'; |
| import MessageModel from './Message'; |
| |
| // Keys of the PromotedMessages protobuf message which contain lists of promoted |
| // messages. |
| const kPromotedMessagesKeys = [1, 2, 3, 4, 5, 6]; |
| |
| /** |
| * Model for the `ThreadView` protobuf message. |
| */ |
| export default class ThreadModel { |
| private data: ProtobufObject; |
| |
| constructor(data?: ProtobufObject) { |
| this.data = data ?? {}; |
| } |
| |
| getId() { |
| return (this.data[2]?.[1]?.[1] as ProtobufNumber) ?? null; |
| } |
| |
| getForumId() { |
| return (this.data[2]?.[1]?.[3] as ProtobufNumber) ?? null; |
| } |
| |
| getRawCommentsAndGaps(): ProtobufObject[] { |
| return (this.data[40] as ProtobufObject[]) ?? []; |
| } |
| |
| setRawCommentsAndGaps(cogs: ProtobufObject[] | null) { |
| 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); |
| throw new Error('Expected message or gap.'); |
| }); |
| } |
| |
| setLastMessage(message: ProtobufObject | null) { |
| if (!this.data[17]) this.data[17] = []; |
| this.data[17][3] = message; |
| } |
| |
| setNumMessages(num: ProtobufNumber | null) { |
| 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(): ProtobufObject { |
| return this.data; |
| } |
| |
| getPromotedMessagesList(): MessageModel[] { |
| const promotedMessages: MessageModel[] = []; |
| 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(): MessageModel[] { |
| const messages: MessageModel[] = []; |
| |
| 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: Array<MessageModel | GapModel>, |
| b: Array<MessageModel | GapModel>, |
| ): Array<MessageModel | GapModel> { |
| if (a.length == 0 || b.length == 0) |
| return a.length > 0 ? a : b.length > 0 ? b : []; |
| |
| let e: Array<MessageModel | GapModel> = []; |
| 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: bigint; |
| 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: bigint; |
| 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: Array<Array<MessageModel | GapModel>>, |
| ) { |
| 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; |
| } |
| } |