diff --git a/src/@types/event.ts b/src/@types/event.ts index 05f85cc47..25946a5ac 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -93,14 +93,7 @@ export enum RelationType { Annotation = "m.annotation", Replace = "m.replace", Reference = "m.reference", - /** - * Note, "io.element.thread" is hardcoded - * Should be replaced with "m.thread" once MSC3440 lands - * Can not use `UnstableValue` as TypeScript does not - * allow computed values in enums - * https://github.com/microsoft/TypeScript/issues/27976 - */ - Thread = "io.element.thread", + Thread = "m.thread", } export enum MsgType { diff --git a/src/models/event.ts b/src/models/event.ts index a4d0340a0..5d1dc784e 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -86,9 +86,17 @@ export interface IEvent { unsigned: IUnsigned; redacts?: string; - // v1 legacy fields + /** + * @deprecated + */ user_id?: string; + /** + * @deprecated + */ prev_content?: IContent; + /** + * @deprecated + */ age?: number; } diff --git a/src/models/room.ts b/src/models/room.ts index 283cf55bc..2490ad7c9 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -23,7 +23,7 @@ import { Direction, EventTimeline } from "./event-timeline"; import { getHttpUriForMxc } from "../content-repo"; import * as utils from "../utils"; import { normalize } from "../utils"; -import { IEvent, MatrixEvent } from "./event"; +import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event"; import { EventStatus } from "./event-status"; import { RoomMember } from "./room-member"; import { IRoomSummary, RoomSummary } from "./room-summary"; @@ -32,6 +32,7 @@ import { TypedReEmitter } from '../ReEmitter'; import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS, EVENT_VISIBILITY_CHANGE_TYPE, + RelationType, } from "../@types/event"; import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client"; import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; @@ -148,6 +149,7 @@ export interface ICreateFilterOpts { // timeline. Useful to disable for some filters that can't be achieved by the // client in an efficient manner prepopulateTimeline?: boolean; + useSyncEvents?: boolean; pendingEvents?: boolean; } @@ -167,6 +169,7 @@ export enum RoomEvent { type EmittedEvents = RoomEvent | ThreadEvent.New | ThreadEvent.Update + | ThreadEvent.NewReply | RoomEvent.Timeline | RoomEvent.TimelineReset; @@ -1346,6 +1349,7 @@ export class Room extends TypedEventEmitter filter: Filter, { prepopulateTimeline = true, + useSyncEvents = true, pendingEvents = true, }: ICreateFilterOpts = {}, ): EventTimelineSet { @@ -1358,8 +1362,10 @@ export class Room extends TypedEventEmitter RoomEvent.Timeline, RoomEvent.TimelineReset, ]); - this.filteredTimelineSets[filter.filterId] = timelineSet; - this.timelineSets.push(timelineSet); + if (useSyncEvents) { + this.filteredTimelineSets[filter.filterId] = timelineSet; + this.timelineSets.push(timelineSet); + } const unfilteredLiveTimeline = this.getLiveTimeline(); // Not all filter are possible to replicate client-side only @@ -1387,7 +1393,7 @@ export class Room extends TypedEventEmitter timeline.getPaginationToken(EventTimeline.BACKWARDS), EventTimeline.BACKWARDS, ); - } else { + } else if (useSyncEvents) { const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward); timelineSet .getLiveTimeline() @@ -1405,41 +1411,54 @@ export class Room extends TypedEventEmitter return timelineSet; } + private async getThreadListFilter(filterType = ThreadFilterType.All): Promise { + const myUserId = this.client.getUserId(); + const filter = new Filter(myUserId); + + const definition: IFilterDefinition = { + "room": { + "timeline": { + [FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name], + }, + }, + }; + + if (filterType === ThreadFilterType.My) { + definition.room.timeline[FILTER_RELATED_BY_SENDERS.name] = [myUserId]; + } + + filter.setDefinition(definition); + const filterId = await this.client.getOrCreateFilter( + `THREAD_PANEL_${this.roomId}_${filterType}`, + filter, + ); + + filter.filterId = filterId; + + return filter; + } + private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise { let timelineSet: EventTimelineSet; if (Thread.hasServerSideSupport) { - const myUserId = this.client.getUserId(); - const filter = new Filter(myUserId); + const filter = await this.getThreadListFilter(filterType); - const definition: IFilterDefinition = { - "room": { - "timeline": { - [FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name], - }, - }, - }; - - if (filterType === ThreadFilterType.My) { - definition.room.timeline[FILTER_RELATED_BY_SENDERS.name] = [myUserId]; - } - - filter.setDefinition(definition); - const filterId = await this.client.getOrCreateFilter( - `THREAD_PANEL_${this.roomId}_${filterType}`, - filter, - ); - filter.filterId = filterId; timelineSet = this.getOrCreateFilteredTimelineSet( filter, { prepopulateTimeline: false, + useSyncEvents: false, pendingEvents: false, }, ); // An empty pagination token allows to paginate from the very bottom of // the timeline set. - timelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); + // Right now we completely by-pass the pagination to be able to order + // the events by last reply to a thread + // Once the server can help us with that, we should uncomment the line + // below + // timelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); } else { timelineSet = new EventTimelineSet(this, { pendingEvents: false, @@ -1460,6 +1479,78 @@ export class Room extends TypedEventEmitter return timelineSet; } + public threadsReady = false; + + public async fetchRoomThreads(): Promise { + if (this.threadsReady) { + return; + } + + const allThreadsFilter = await this.getThreadListFilter(); + + const { chunk: events } = await this.client.createMessagesRequest( + this.roomId, + "", + Number.MAX_SAFE_INTEGER, + Direction.Backward, + allThreadsFilter, + ); + + const orderedByLastReplyEvents = events + .map(this.client.getEventMapper()) + .sort((eventA, eventB) => { + /** + * `origin_server_ts` in a decentralised world is far from ideal + * but for lack of any better, we will have to use this + * Long term the sorting should be handled by homeservers and this + * is only meant as a short term patch + */ + const threadAMetadata = eventA + .getServerAggregatedRelation(RelationType.Thread); + const threadBMetadata = eventB + .getServerAggregatedRelation(RelationType.Thread); + return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; + }); + + const myThreads = orderedByLastReplyEvents.filter(event => { + const threadRelationship = event + .getServerAggregatedRelation(RelationType.Thread); + return threadRelationship.current_user_participated; + }); + + const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); + for (const event of orderedByLastReplyEvents) { + this.threadsTimelineSets[0].addLiveEvent( + event, + DuplicateStrategy.Ignore, + false, + roomState, + ); + } + for (const event of myThreads) { + this.threadsTimelineSets[1].addLiveEvent( + event, + DuplicateStrategy.Ignore, + false, + roomState, + ); + } + + this.client.decryptEventIfNeeded(orderedByLastReplyEvents[orderedByLastReplyEvents.length -1]); + this.client.decryptEventIfNeeded(myThreads[myThreads.length -1]); + + this.threadsReady = true; + + this.on(ThreadEvent.NewReply, this.onThreadNewReply); + } + + private onThreadNewReply(thread: Thread): void { + for (const timelineSet of this.threadsTimelineSets) { + timelineSet.removeEvent(thread.id); + timelineSet.addLiveEvent(thread.rootEvent); + } + } + /** * Forget the timelineSet for this room with the given filter * @@ -1550,6 +1641,7 @@ export class Room extends TypedEventEmitter this.threads.set(thread.id, thread); this.reEmitter.reEmit(thread, [ ThreadEvent.Update, + ThreadEvent.NewReply, RoomEvent.Timeline, RoomEvent.TimelineReset, ]); @@ -1560,19 +1652,21 @@ export class Room extends TypedEventEmitter this.emit(ThreadEvent.New, thread, toStartOfTimeline); - this.threadsTimelineSets.forEach(timelineSet => { - if (thread.rootEvent) { - if (Thread.hasServerSideSupport) { - timelineSet.addLiveEvent(thread.rootEvent); - } else { - timelineSet.addEventToTimeline( - thread.rootEvent, - timelineSet.getLiveTimeline(), - toStartOfTimeline, - ); + if (this.threadsReady) { + this.threadsTimelineSets.forEach(timelineSet => { + if (thread.rootEvent) { + if (Thread.hasServerSideSupport) { + timelineSet.addLiveEvent(thread.rootEvent); + } else { + timelineSet.addEventToTimeline( + thread.rootEvent, + timelineSet.getLiveTimeline(), + toStartOfTimeline, + ); + } } - } - }); + }); + } return thread; }