You've already forked matrix-js-sdk
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user