1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Thread list ordering by last reply (#2253)

This commit is contained in:
Germain
2022-03-23 14:43:30 +00:00
committed by GitHub
parent d0b964837f
commit 6192325fe0
3 changed files with 141 additions and 46 deletions

View File

@@ -93,14 +93,7 @@ export enum RelationType {
Annotation = "m.annotation", Annotation = "m.annotation",
Replace = "m.replace", Replace = "m.replace",
Reference = "m.reference", Reference = "m.reference",
/** Thread = "m.thread",
* 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",
} }
export enum MsgType { export enum MsgType {

View File

@@ -86,9 +86,17 @@ export interface IEvent {
unsigned: IUnsigned; unsigned: IUnsigned;
redacts?: string; redacts?: string;
// v1 legacy fields /**
* @deprecated
*/
user_id?: string; user_id?: string;
/**
* @deprecated
*/
prev_content?: IContent; prev_content?: IContent;
/**
* @deprecated
*/
age?: number; age?: number;
} }

View File

@@ -23,7 +23,7 @@ import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo"; import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils"; import * as utils from "../utils";
import { normalize } from "../utils"; import { normalize } from "../utils";
import { IEvent, MatrixEvent } from "./event"; import { IEvent, IThreadBundledRelationship, MatrixEvent } from "./event";
import { EventStatus } from "./event-status"; import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member"; import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary"; import { IRoomSummary, RoomSummary } from "./room-summary";
@@ -32,6 +32,7 @@ import { TypedReEmitter } from '../ReEmitter';
import { import {
EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS, EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
EVENT_VISIBILITY_CHANGE_TYPE, EVENT_VISIBILITY_CHANGE_TYPE,
RelationType,
} from "../@types/event"; } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client"; import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials"; 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 // timeline. Useful to disable for some filters that can't be achieved by the
// client in an efficient manner // client in an efficient manner
prepopulateTimeline?: boolean; prepopulateTimeline?: boolean;
useSyncEvents?: boolean;
pendingEvents?: boolean; pendingEvents?: boolean;
} }
@@ -167,6 +169,7 @@ export enum RoomEvent {
type EmittedEvents = RoomEvent type EmittedEvents = RoomEvent
| ThreadEvent.New | ThreadEvent.New
| ThreadEvent.Update | ThreadEvent.Update
| ThreadEvent.NewReply
| RoomEvent.Timeline | RoomEvent.Timeline
| RoomEvent.TimelineReset; | RoomEvent.TimelineReset;
@@ -1346,6 +1349,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
filter: Filter, filter: Filter,
{ {
prepopulateTimeline = true, prepopulateTimeline = true,
useSyncEvents = true,
pendingEvents = true, pendingEvents = true,
}: ICreateFilterOpts = {}, }: ICreateFilterOpts = {},
): EventTimelineSet { ): EventTimelineSet {
@@ -1358,8 +1362,10 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
RoomEvent.Timeline, RoomEvent.Timeline,
RoomEvent.TimelineReset, RoomEvent.TimelineReset,
]); ]);
this.filteredTimelineSets[filter.filterId] = timelineSet; if (useSyncEvents) {
this.timelineSets.push(timelineSet); this.filteredTimelineSets[filter.filterId] = timelineSet;
this.timelineSets.push(timelineSet);
}
const unfilteredLiveTimeline = this.getLiveTimeline(); const unfilteredLiveTimeline = this.getLiveTimeline();
// Not all filter are possible to replicate client-side only // Not all filter are possible to replicate client-side only
@@ -1387,7 +1393,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
timeline.getPaginationToken(EventTimeline.BACKWARDS), timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS, EventTimeline.BACKWARDS,
); );
} else { } else if (useSyncEvents) {
const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward); const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
timelineSet timelineSet
.getLiveTimeline() .getLiveTimeline()
@@ -1405,41 +1411,54 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return timelineSet; return timelineSet;
} }
private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> {
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<EventTimelineSet> { private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
let timelineSet: EventTimelineSet; let timelineSet: EventTimelineSet;
if (Thread.hasServerSideSupport) { if (Thread.hasServerSideSupport) {
const myUserId = this.client.getUserId(); const filter = await this.getThreadListFilter(filterType);
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;
timelineSet = this.getOrCreateFilteredTimelineSet( timelineSet = this.getOrCreateFilteredTimelineSet(
filter, filter,
{ {
prepopulateTimeline: false, prepopulateTimeline: false,
useSyncEvents: false,
pendingEvents: false, pendingEvents: false,
}, },
); );
// An empty pagination token allows to paginate from the very bottom of // An empty pagination token allows to paginate from the very bottom of
// the timeline set. // 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 { } else {
timelineSet = new EventTimelineSet(this, { timelineSet = new EventTimelineSet(this, {
pendingEvents: false, pendingEvents: false,
@@ -1460,6 +1479,78 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return timelineSet; return timelineSet;
} }
public threadsReady = false;
public async fetchRoomThreads(): Promise<void> {
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<IThreadBundledRelationship>(RelationType.Thread);
const threadBMetadata = eventB
.getServerAggregatedRelation<IThreadBundledRelationship>(RelationType.Thread);
return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
});
const myThreads = orderedByLastReplyEvents.filter(event => {
const threadRelationship = event
.getServerAggregatedRelation<IThreadBundledRelationship>(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 * Forget the timelineSet for this room with the given filter
* *
@@ -1550,6 +1641,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
this.threads.set(thread.id, thread); this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [ this.reEmitter.reEmit(thread, [
ThreadEvent.Update, ThreadEvent.Update,
ThreadEvent.NewReply,
RoomEvent.Timeline, RoomEvent.Timeline,
RoomEvent.TimelineReset, RoomEvent.TimelineReset,
]); ]);
@@ -1560,19 +1652,21 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
this.emit(ThreadEvent.New, thread, toStartOfTimeline); this.emit(ThreadEvent.New, thread, toStartOfTimeline);
this.threadsTimelineSets.forEach(timelineSet => { if (this.threadsReady) {
if (thread.rootEvent) { this.threadsTimelineSets.forEach(timelineSet => {
if (Thread.hasServerSideSupport) { if (thread.rootEvent) {
timelineSet.addLiveEvent(thread.rootEvent); if (Thread.hasServerSideSupport) {
} else { timelineSet.addLiveEvent(thread.rootEvent);
timelineSet.addEventToTimeline( } else {
thread.rootEvent, timelineSet.addEventToTimeline(
timelineSet.getLiveTimeline(), thread.rootEvent,
toStartOfTimeline, timelineSet.getLiveTimeline(),
); toStartOfTimeline,
);
}
} }
} });
}); }
return thread; return thread;
} }