diff --git a/src/client.ts b/src/client.ts index 54e0f1cb7c5..66b5fe225a4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5243,6 +5243,10 @@ export class MatrixClient extends TypedEventEmitter = res.end; while (nextBatch) { - const resNewer = await this.fetchRelations( + const resNewer: IRelationsResponse = await this.fetchRelations( timelineSet.room.roomId, thread.id, THREAD_RELATION_TYPE.name, @@ -5532,7 +5536,6 @@ export class MatrixClient extends TypedEventEmitter { const mapper = this.getEventMapper(); const matrixEvents = res.chunk.map(mapper); @@ -6933,12 +6936,12 @@ export class MatrixClient extends TypedEventEmitter { - const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType); + const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null; const result = await this.fetchRelations( roomId, eventId, @@ -6962,7 +6965,7 @@ export class MatrixClient extends TypedEventEmitter e.getSender() === originalEvent.getSender()); } return { - originalEvent, + originalEvent: originalEvent ?? null, events, nextBatch: result.next_batch, prevBatch: result.prev_batch, diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 55bd5615153..da38ad1a67e 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -18,6 +18,8 @@ limitations under the License. * @module models/event-timeline-set */ +import { Optional } from "matrix-events-sdk"; + import { EventTimeline, IAddEventOptions } from "./event-timeline"; import { MatrixEvent } from "./event"; import { logger } from '../logger'; @@ -290,8 +292,8 @@ export class EventTimelineSet extends TypedEventEmitter): EventTimeline | null { + if (eventId === null || eventId === undefined) { return null; } const res = this._eventIdToTimeline.get(eventId); return (res === undefined) ? null : res; } @@ -352,7 +354,7 @@ export class EventTimelineSet extends TypedEventEmitter, ): void { if (!timeline) { throw new Error( diff --git a/src/models/event.ts b/src/models/event.ts index 2136c91fad1..36ad8d54787 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -259,8 +259,8 @@ export class MatrixEvent extends TypedEventEmitter { * @experimental */ public threads = new Map(); - public lastThread: Thread; + public lastThread: Thread | null = null; /** * A mapping of eventId to all visibility changes to apply @@ -1016,7 +1016,8 @@ export class Room extends ReadReceipt { public resetLiveTimeline(backPaginationToken?: string | null, forwardPaginationToken?: string | null): void { for (let i = 0; i < this.timelineSets.length; i++) { this.timelineSets[i].resetLiveTimeline( - backPaginationToken, forwardPaginationToken, + backPaginationToken ?? undefined, + forwardPaginationToken ?? undefined, ); } @@ -1860,9 +1861,7 @@ export class Room extends ReadReceipt { } private onThreadNewReply(thread: Thread): void { - if (thread.length && thread.rootEvent) { - this.updateThreadRootEvents(thread, false); - } + this.updateThreadRootEvents(thread, false); } private onThreadDelete(thread: Thread): void { @@ -1989,9 +1988,11 @@ export class Room extends ReadReceipt { } private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean) => { - this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline); - if (thread.hasCurrentUserParticipated) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline); + if (thread.length) { + this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline); + if (thread.hasCurrentUserParticipated) { + this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline); + } } }; @@ -2000,18 +2001,20 @@ export class Room extends ReadReceipt { thread: Thread, toStartOfTimeline: boolean, ) => { - if (Thread.hasServerSideSupport) { - timelineSet.addLiveEvent(thread.rootEvent, { - duplicateStrategy: DuplicateStrategy.Replace, - fromCache: false, - roomState: this.currentState, - }); - } else { - timelineSet.addEventToTimeline( - thread.rootEvent, - timelineSet.getLiveTimeline(), - { toStartOfTimeline }, - ); + if (timelineSet && thread.rootEvent) { + if (Thread.hasServerSideSupport) { + timelineSet.addLiveEvent(thread.rootEvent, { + duplicateStrategy: DuplicateStrategy.Replace, + fromCache: false, + roomState: this.currentState, + }); + } else { + timelineSet.addEventToTimeline( + thread.rootEvent, + timelineSet.getLiveTimeline(), + { toStartOfTimeline }, + ); + } } }; @@ -2052,15 +2055,16 @@ export class Room extends ReadReceipt { RoomEvent.Timeline, RoomEvent.TimelineReset, ]); + const isNewer = this.lastThread?.rootEvent + && rootEvent?.localTimestamp + && this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp; - if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) { + if (!this.lastThread || isNewer) { this.lastThread = thread; } if (this.threadsReady) { - if (thread.length && thread.rootEvent) { - this.updateThreadRootEvents(thread, toStartOfTimeline); - } + this.updateThreadRootEvents(thread, toStartOfTimeline); } this.emit(ThreadEvent.New, thread, toStartOfTimeline); diff --git a/src/models/thread.ts b/src/models/thread.ts index 13496022259..7946b81bfd0 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -90,6 +90,8 @@ export class Thread extends ReadReceipt { public readonly room: Room; public readonly client: MatrixClient; + public initialEventsFetched = !Thread.hasServerSideSupport; + constructor( public readonly id: string, public rootEvent: MatrixEvent | undefined, @@ -232,6 +234,9 @@ export class Thread extends ReadReceipt { public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise { this.setEventMetadata(event); + const lastReply = this.lastReply(); + const isNewestReply = !lastReply || event.localTimestamp > lastReply?.localTimestamp; + // Add all incoming events to the thread's timeline set when there's no server support if (!Thread.hasServerSideSupport) { // all the relevant membership info to hydrate events with a sender @@ -242,14 +247,15 @@ export class Thread extends ReadReceipt { this.client.decryptEventIfNeeded(event, {}); } else if (!toStartOfTimeline && - event.localTimestamp > this.lastReply()?.localTimestamp + this.initialEventsFetched && + isNewestReply ) { await this.fetchEditsWhereNeeded(event); this.addEventToTimeline(event, false); } else if (event.isRelation(RelationType.Annotation) || event.isRelation(RelationType.Replace)) { // Apply annotations and replace relations to the relations of the timeline only - this.timelineSet.relations.aggregateParentEvent(event); - this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet); + this.timelineSet.relations?.aggregateParentEvent(event); + this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet); return; } @@ -265,12 +271,14 @@ export class Thread extends ReadReceipt { } } - public async processEvent(event: MatrixEvent): Promise { - this.setEventMetadata(event); - await this.fetchEditsWhereNeeded(event); + public async processEvent(event: Optional): Promise { + if (event) { + this.setEventMetadata(event); + await this.fetchEditsWhereNeeded(event); + } } - private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship { + private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined { return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); } @@ -283,13 +291,29 @@ export class Thread extends ReadReceipt { if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; - this._currentUserParticipated = bundledRelationship.current_user_participated; + this._currentUserParticipated = bundledRelationship.current_user_participated ?? false; const mapper = this.client.getEventMapper(); this.lastEvent = mapper(bundledRelationship.latest_event); await this.processEvent(this.lastEvent); } + if (!this.initialEventsFetched) { + this.initialEventsFetched = true; + // fetch initial event to allow proper pagination + try { + // if the thread has regular events, this will just load the last reply. + // if the thread is newly created, this will load the root event. + await this.client.paginateEventTimeline(this.liveTimeline, { backwards: true, limit: 1 }); + // just to make sure that, if we've created a timeline window for this thread before the thread itself + // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly. + this.emit(RoomEvent.TimelineReset, this.room, this.timelineSet, true); + } catch (e) { + logger.error("Failed to load start of newly created thread: ", e); + this.initialEventsFetched = false; + } + } + this.emit(ThreadEvent.Update, this); } diff --git a/src/utils.ts b/src/utils.ts index dc109128f7e..6f3a47dbd45 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,6 +22,7 @@ limitations under the License. import unhomoglyph from "unhomoglyph"; import promiseRetry from "p-retry"; +import { Optional } from "matrix-events-sdk"; import type * as NodeCrypto from "crypto"; import { MatrixEvent } from "./models/event"; @@ -123,13 +124,17 @@ export function decodeParams(query: string): Record { * variables with. E.g. { "$bar": "baz" }. * @return {string} The result of replacing all template variables e.g. '/foo/baz'. */ -export function encodeUri(pathTemplate: string, variables: Record): string { +export function encodeUri(pathTemplate: string, variables: Record>): string { for (const key in variables) { if (!variables.hasOwnProperty(key)) { continue; } + const value = variables[key]; + if (value === undefined || value === null) { + continue; + } pathTemplate = pathTemplate.replace( - key, encodeURIComponent(variables[key]), + key, encodeURIComponent(value), ); } return pathTemplate;