Skip to content

Commit b419f37

Browse files
committed
initial implementation of thread listing msc
1 parent c0a4d51 commit b419f37

File tree

5 files changed

+250
-37
lines changed

5 files changed

+250
-37
lines changed

src/client.ts

Lines changed: 149 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ limitations under the License.
1919
* @module client
2020
*/
2121

22-
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk";
22+
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk";
2323

2424
import { ISyncStateData, SyncApi, SyncState } from "./sync";
2525
import {
@@ -33,7 +33,7 @@ import {
3333
} from "./models/event";
3434
import { StubStore } from "./store/stub";
3535
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
36-
import { Filter, IFilterDefinition } from "./filter";
36+
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
3737
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
3838
import * as utils from './utils';
3939
import { sleep } from './utils';
@@ -591,6 +591,13 @@ interface IMessagesResponse {
591591
state: IStateEvent[];
592592
}
593593

594+
interface IThreadedMessagesResponse {
595+
prev_batch: string;
596+
next_batch: string;
597+
chunk: IRoomEvent[];
598+
state: IStateEvent[];
599+
}
600+
594601
export interface IRequestTokenResponse {
595602
sid: string;
596603
submit_url?: string;
@@ -5343,13 +5350,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
53435350
* @return {Promise} Resolves:
53445351
* {@link module:models/event-timeline~EventTimeline} including the given event
53455352
*/
5346-
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
5353+
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<Optional<EventTimeline>> {
53475354
// don't allow any timeline support unless it's been enabled.
53485355
if (!this.timelineSupport) {
53495356
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
53505357
" parameter to true when creating MatrixClient to enable it.");
53515358
}
53525359

5360+
if (!timelineSet.room) {
5361+
throw new Error("getEventTimeline only supports room timelines");
5362+
}
5363+
53535364
if (timelineSet.getTimelineForEvent(eventId)) {
53545365
return timelineSet.getTimelineForEvent(eventId);
53555366
}
@@ -5361,7 +5372,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
53615372
},
53625373
);
53635374

5364-
let params: Record<string, string | string[]> = undefined;
5375+
let params: Record<string, string | string[]> | undefined = undefined;
53655376
if (this.clientOpts.lazyLoadMembers) {
53665377
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
53675378
}
@@ -5454,27 +5465,36 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
54545465
* @return {Promise} Resolves:
54555466
* {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room
54565467
*/
5457-
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<EventTimeline> {
5468+
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> {
54585469
// don't allow any timeline support unless it's been enabled.
54595470
if (!this.timelineSupport) {
54605471
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
54615472
" parameter to true when creating MatrixClient to enable it.");
54625473
}
54635474

5464-
const messagesPath = utils.encodeUri(
5465-
"/rooms/$roomId/messages", {
5466-
$roomId: timelineSet.room.roomId,
5467-
},
5468-
);
5469-
5470-
const params: Record<string, string | string[]> = {
5471-
dir: 'b',
5472-
};
5473-
if (this.clientOpts.lazyLoadMembers) {
5474-
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
5475+
if (!timelineSet.room) {
5476+
throw new Error("getLatestTimeline only supports room timelines");
54755477
}
54765478

5477-
const res = await this.http.authedRequest<IMessagesResponse>(undefined, Method.Get, messagesPath, params);
5479+
let res: IMessagesResponse;
5480+
const roomId = timelineSet.room?.roomId;
5481+
if (timelineSet.isThreadTimeline) {
5482+
res = await this.createThreadListMessagesRequest(
5483+
roomId,
5484+
null,
5485+
1,
5486+
Direction.Backward,
5487+
timelineSet.getFilter(),
5488+
);
5489+
} else {
5490+
res = await this.createMessagesRequest(
5491+
roomId,
5492+
null,
5493+
1,
5494+
Direction.Backward,
5495+
timelineSet.getFilter(),
5496+
);
5497+
}
54785498
const event = res.chunk?.[0];
54795499
if (!event) {
54805500
throw new Error("No message returned from /messages when trying to construct getLatestTimeline");
@@ -5514,7 +5534,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55145534
params.from = fromToken;
55155535
}
55165536

5517-
let filter = null;
5537+
let filter: IRoomEventFilter | null = null;
55185538
if (this.clientOpts.lazyLoadMembers) {
55195539
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
55205540
// so the timelineFilter doesn't get written into it below
@@ -5532,6 +5552,67 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55325552
return this.http.authedRequest(undefined, Method.Get, path, params);
55335553
}
55345554

5555+
/**
5556+
* Makes a request to /messages with the appropriate lazy loading filter set.
5557+
* XXX: if we do get rid of scrollback (as it's not used at the moment),
5558+
* we could inline this method again in paginateEventTimeline as that would
5559+
* then be the only call-site
5560+
* @param {string} roomId
5561+
* @param {string} fromToken
5562+
* @param {number} limit the maximum amount of events the retrieve
5563+
* @param {string} dir 'f' or 'b'
5564+
* @param {Filter} timelineFilter the timeline filter to pass
5565+
* @return {Promise}
5566+
*/
5567+
// XXX: Intended private, used by room.fetchRoomThreads
5568+
public createThreadListMessagesRequest(
5569+
roomId: string,
5570+
fromToken: string | null,
5571+
limit = 30,
5572+
dir = Direction.Backward,
5573+
timelineFilter?: Filter,
5574+
): Promise<IMessagesResponse> {
5575+
const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
5576+
5577+
const params: Record<string, string> = {
5578+
limit: limit.toString(),
5579+
dir: dir,
5580+
include: 'all',
5581+
};
5582+
5583+
if (fromToken) {
5584+
params.from = fromToken;
5585+
}
5586+
5587+
let filter: IRoomEventFilter | null = null;
5588+
if (this.clientOpts.lazyLoadMembers) {
5589+
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
5590+
// so the timelineFilter doesn't get written into it below
5591+
filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
5592+
}
5593+
if (timelineFilter) {
5594+
// XXX: it's horrific that /messages' filter parameter doesn't match
5595+
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
5596+
filter = filter || {};
5597+
Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()?.toJSON());
5598+
}
5599+
if (filter) {
5600+
params.filter = JSON.stringify(filter);
5601+
}
5602+
5603+
const opts: { prefix?: string } = {};
5604+
if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) {
5605+
opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856";
5606+
}
5607+
5608+
return this.http.authedRequest<IThreadedMessagesResponse>(undefined, Method.Get, path, params, undefined, opts)
5609+
.then(res => ({
5610+
...res,
5611+
start: res.prev_batch,
5612+
end: res.next_batch,
5613+
}));
5614+
}
5615+
55355616
/**
55365617
* Take an EventTimeline, and back/forward-fill results.
55375618
*
@@ -5547,6 +5628,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55475628
*/
55485629
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
55495630
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
5631+
const room = this.getRoom(eventTimeline.getRoomId());
5632+
const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline;
55505633

55515634
// TODO: we should implement a backoff (as per scrollback()) to deal more
55525635
// nicely with HTTP errors.
@@ -5580,15 +5663,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
55805663
only: 'highlight',
55815664
};
55825665

5583-
if (token !== "end") {
5666+
if (token && token !== "end") {
55845667
params.from = token;
55855668
}
55865669

55875670
promise = this.http.authedRequest<INotificationsResponse>(
55885671
undefined, Method.Get, path, params,
55895672
).then(async (res) => {
55905673
const token = res.next_token;
5591-
const matrixEvents = [];
5674+
const matrixEvents: MatrixEvent[] = [];
55925675

55935676
for (let i = 0; i < res.notifications.length; i++) {
55945677
const notification = res.notifications[i];
@@ -5612,13 +5695,48 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
56125695
if (backwards && !res.next_token) {
56135696
eventTimeline.setPaginationToken(null, dir);
56145697
}
5615-
return res.next_token ? true : false;
5698+
return Boolean(res.next_token);
5699+
}).finally(() => {
5700+
eventTimeline.paginationRequests[dir] = null;
5701+
});
5702+
eventTimeline.paginationRequests[dir] = promise;
5703+
} else if (isThreadTimeline) {
5704+
if (!room) {
5705+
throw new Error("Unknown room " + eventTimeline.getRoomId());
5706+
}
5707+
5708+
promise = this.createThreadListMessagesRequest(
5709+
eventTimeline.getRoomId(),
5710+
token,
5711+
opts.limit,
5712+
dir,
5713+
eventTimeline.getFilter(),
5714+
).then((res) => {
5715+
if (res.state) {
5716+
const roomState = eventTimeline.getState(dir);
5717+
const stateEvents = res.state.map(this.getEventMapper());
5718+
roomState.setUnknownStateEvents(stateEvents);
5719+
}
5720+
const token = res.end;
5721+
const matrixEvents = res.chunk.map(this.getEventMapper());
5722+
5723+
const timelineSet = eventTimeline.getTimelineSet();
5724+
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
5725+
this.processBeaconEvents(room, matrixEvents);
5726+
this.processThreadRoots(room, matrixEvents, backwards);
5727+
5728+
// if we've hit the end of the timeline, we need to stop trying to
5729+
// paginate. We need to keep the 'forwards' token though, to make sure
5730+
// we can recover from gappy syncs.
5731+
if (backwards && res.end == res.start) {
5732+
eventTimeline.setPaginationToken(null, dir);
5733+
}
5734+
return res.end != res.start;
56165735
}).finally(() => {
56175736
eventTimeline.paginationRequests[dir] = null;
56185737
});
56195738
eventTimeline.paginationRequests[dir] = promise;
56205739
} else {
5621-
const room = this.getRoom(eventTimeline.getRoomId());
56225740
if (!room) {
56235741
throw new Error("Unknown room " + eventTimeline.getRoomId());
56245742
}
@@ -5639,9 +5757,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
56395757
const matrixEvents = res.chunk.map(this.getEventMapper());
56405758

56415759
const timelineSet = eventTimeline.getTimelineSet();
5642-
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
5760+
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
56435761
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
5644-
this.processBeaconEvents(timelineSet.room, timelineEvents);
5762+
this.processBeaconEvents(room, timelineEvents);
56455763
this.processThreadEvents(room, threadedEvents, backwards);
56465764

56475765
const atEnd = res.end === undefined || res.end === res.start;
@@ -9183,6 +9301,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
91839301
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
91849302
}
91859303

9304+
/**
9305+
* @experimental
9306+
*/
9307+
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
9308+
room.processThreadRoots(threadedEvents, toStartOfTimeline);
9309+
}
9310+
91869311
public processBeaconEvents(
91879312
room?: Room,
91889313
events?: MatrixEvent[],

src/models/event-timeline-set.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
123123
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
124124
* can be omitted if room is specified.
125125
* @param {Thread=} thread the thread to which this timeline set relates.
126+
* @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline
127+
* (e.g., All threads or My threads)
126128
*/
127129
constructor(
128130
public readonly room: Room | undefined,
129131
opts: IOpts = {},
130132
client?: MatrixClient,
131133
public readonly thread?: Thread,
134+
public readonly isThreadTimeline: boolean = false,
132135
) {
133136
super();
134137

src/models/event-timeline.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class EventTimeline {
9999
private endState: RoomState;
100100
private prevTimeline?: EventTimeline;
101101
private nextTimeline?: EventTimeline;
102-
public paginationRequests: Record<Direction, Promise<boolean>> = {
102+
public paginationRequests: Record<Direction, Promise<boolean> | null> = {
103103
[Direction.Backward]: null,
104104
[Direction.Forward]: null,
105105
};
@@ -311,7 +311,7 @@ export class EventTimeline {
311311
* token for going backwards in time; EventTimeline.FORWARDS to set the
312312
* pagination token for going forwards in time.
313313
*/
314-
public setPaginationToken(token: string, direction: Direction): void {
314+
public setPaginationToken(token: string | null, direction: Direction): void {
315315
this.getState(direction).paginationToken = token;
316316
}
317317

src/models/room-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
9797
// XXX: Should be read-only
9898
public members: Record<string, RoomMember> = {}; // userId: RoomMember
9999
public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
100-
public paginationToken: string = null;
100+
public paginationToken: string | null = null;
101101

102102
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
103103
private _liveBeaconIds: BeaconIdentifier[] = [];

0 commit comments

Comments
 (0)