Skip to content

Draft: Refresh room timeline when we see a MSC2716 marker event #2282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2b029c9
Refresh room timeline when we see a MSC2716 marker event
MadLittleMods Apr 8, 2022
ffbcbff
Don't refresh timeline when first sync or from cache
MadLittleMods Apr 8, 2022
39cd6e9
Fix docstring typo
MadLittleMods Apr 8, 2022
e5f34db
Only notify client that timeline should be refreshed instead of doing…
MadLittleMods Apr 13, 2022
e070b3c
Remove console.log
MadLittleMods Apr 13, 2022
21f6aa2
Fixup docstrings
MadLittleMods Apr 13, 2022
9bfcf4b
Fix some lints
MadLittleMods Apr 13, 2022
1c2b955
Merge branch 'develop' into madlittlemods/refresh-timeline-when-we-se…
MadLittleMods Apr 13, 2022
05b9800
Fix some lints
MadLittleMods Apr 13, 2022
8b82bb5
Rename to roomState to align with other options objects
MadLittleMods Apr 13, 2022
35f3f04
Rename to toStartOfTimeline to align with other options objects
MadLittleMods Apr 13, 2022
b2636c3
Revert arg rename change for unused
MadLittleMods Apr 13, 2022
da57a74
Fix up tests
MadLittleMods Apr 13, 2022
c539b64
Fix timeline-window specs
MadLittleMods Apr 13, 2022
11040e5
Stop prompting to refresh timeline when syncing from cache
MadLittleMods Apr 13, 2022
07fcf27
Fix lints and move to logger
MadLittleMods Apr 13, 2022
701bf34
Remove todo
MadLittleMods Apr 13, 2022
c080c6c
Remove fixme in favor of aspirational explanation of what we could do…
MadLittleMods Apr 13, 2022
0694b84
Add some explanation comments
MadLittleMods Apr 13, 2022
26fb2b6
Add some tests
MadLittleMods Apr 13, 2022
8ca2645
WIP: messy timelineWasEmpty
MadLittleMods Apr 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/@types/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export enum EventType {
RoomKeyRequest = "m.room_key_request",
ForwardedRoomKey = "m.forwarded_room_key",
Dummy = "m.dummy",

Marker = "org.matrix.msc2716.marker", // MSC2716
}

export enum RelationType {
Expand Down
1 change: 1 addition & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ type RoomStateEvents = RoomStateEvent.Events
| RoomStateEvent.Members
| RoomStateEvent.NewMember
| RoomStateEvent.Update
| RoomStateEvent.Marker
;

type CryptoEvents = CryptoEvent.KeySignatureUploadFailure
Expand Down
4 changes: 4 additions & 0 deletions src/models/room-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum RoomStateEvent {
NewMember = "RoomState.newMember",
Update = "RoomState.update", // signals batches of updates without specificity
BeaconLiveness = "RoomState.BeaconLiveness",
Marker = "RoomState.org.matrix.msc2716.marker",
}

export type RoomStateEventHandlerMap = {
Expand All @@ -51,6 +52,7 @@ export type RoomStateEventHandlerMap = {
[RoomStateEvent.Update]: (state: RoomState) => void;
[RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void;
[BeaconEvent.New]: (event: MatrixEvent, beacon: Beacon) => void;
[RoomStateEvent.Marker]: (event: MatrixEvent, state: RoomState) => void;
};

type EmittedEvents = RoomStateEvent | BeaconEvent;
Expand Down Expand Up @@ -398,6 +400,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>

// assume all our sentinels are now out-of-date
this.sentinels = {};
} else if (event.getType() === EventType.Marker) {
this.emit(RoomStateEvent.Marker, event, this);
}
});

Expand Down
33 changes: 33 additions & 0 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
public readonly threadsTimelineSets: EventTimelineSet[] = [];
// any filtered timeline sets we're maintaining for this room
private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
private lastMarkerEventIdProcessed: string = null;
private readonly pendingEventList?: MatrixEvent[];
// read by megolm via getter; boolean value - null indicates "use global value"
private blacklistUnverifiedDevices: boolean = null;
Expand Down Expand Up @@ -439,6 +440,19 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return Promise.allSettled(decryptionPromises) as unknown as Promise<void>;
}

/**
* Gets the creator of the room
* @returns {string} The creator of the room, or null if it could not be determined
*/
public getRoomCreator(): string | null {
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
if (!createEvent) {
return null;
}
const roomCreator = createEvent.getContent()['sender'];
return roomCreator;
}

/**
* Gets the version of the room
* @returns {string} The version of the room, or null if it could not be determined
Expand Down Expand Up @@ -1004,6 +1018,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
return this.getUnfilteredTimelineSet().addTimeline();
}

/**
* Get the last marker event ID proccessed
*
* @return {String} the last marker event ID proccessed or null if none have
* been processed
*/
public getLastMarkerEventIdProcessed(): string | null {
return this.lastMarkerEventIdProcessed;
}

/**
* Set the last marker event ID proccessed
*
* @param {String} eventId The marker event ID to set
*/
public setLastMarkerEventIdProcessed(eventId: string): void {
this.lastMarkerEventIdProcessed = eventId;
}

/**
* Get an event which is stored in our unfiltered timeline set, or in a thread
*
Expand Down
29 changes: 29 additions & 0 deletions src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export enum SyncState {
Reconnecting = "RECONNECTING",
}

// Room versions where "insertion", "batch", and "marker" events are controlled
// by power-levels. MSC2716 is supported in existing room versions but they
// should only have special meaning when the room creator sends them.
const MSC2716_ROOM_VERSIONS = [
'org.matrix.msc2716v3'
];

function getFilterName(userId: string, suffix?: string): string {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
Expand Down Expand Up @@ -239,6 +246,27 @@ export class SyncApi {
RoomMemberEvent.Membership,
]);
});

room.currentState.on(RoomStateEvent.Marker, function(event, state) {
const isValidMsc2716Event = MSC2716_ROOM_VERSIONS.includes(room.getVersion()) ||
// MSC2716 is supported in all existing room versions but special
// meaning should only be given to "insertion", "batch", and
// "marker" events when they come from the room creator
event.getSender() === room.getRoomCreator();
console.log(`On marker state isValidMsc2716Event=${isValidMsc2716Event}, event_id=${event.getId()} room.getLastMarkerEventIdProcessed()=${room.getLastMarkerEventIdProcessed()}`);
if(
isValidMsc2716Event &&
// We only need to throw the timeline away once when we see a
// marker. All of the historical content will be in the
// `/messsages` responses from here on out.
event.getId() !== room.getLastMarkerEventIdProcessed()
) {
// Saw new marker event, refreshing the timeline
console.log('Saw new marker event, refreshing the timeline');
room.resetLiveTimeline(null, null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Test if this is the function we actually want to use.

Our goal is to throw away the current timeline and re-fetch events for the room from the server.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be better if we re-fetched the new events first, then replaced the timeline

Copy link
Contributor Author

@MadLittleMods MadLittleMods Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more, do we even want to do this automatically? Someone could cause your Element client to constantly refresh the timeline by just sending marker events over and over. Granted, you probably want to leave a room like this 🤷. Perhaps also some sort of DOS vector since everyone will be refreshing and hitting the server at the exact same time.

I think we might want to approach this to be a similar flow to how Element receives app updates. When the client receives a marker event, we first notify the user that history was imported and is available to see after they refresh their timeline (which can be a button action). The UI can probably use something similar to the "Connectivity to the server has been lost." banner.

In most cases, the history won't be in people's current timelines cached in the Element client, so do we even need to worry about refreshing? This is what I kinda leaned to before this PR but it's slightly important from a demo perspective and a personal bridge flow. For example with Beeper, a room is created for your Whatsapp conversation, and as you sit in the room, you probably ideally want the history to appear as it is imported but at least be apparent in some way after the import finishes.

We can detect whether the history imported affects any timelines on the client and only ask to refresh then. To detect, when we receive a marker event, make a request to /context/{insertionEventId} with the insertionEventId the marker is pointing to and check if any its prev_events are in the timeline. /context doesn't work over federation but the homeserver backfills the insertion event when it receives the marker so it already has it available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to prompt the user to refresh their timeline: matrix-org/matrix-react-sdk#8303

We can detect whether the history imported affects any timelines on the client and only ask to refresh then. To detect, when we receive a marker event, make a request to /context/{insertionEventId} with the insertionEventId the marker is pointing to and check if any its prev_events are in the timeline. /context doesn't work over federation but the homeserver backfills the insertion event when it receives the marker so it already has it available.

We can't do this because prev_events aren't available in the client API's 😢. I'm not too keen to add an extra field to the marker or insertion event indicating where the history is imported next to as there isn't a way to enforce that it's true (a homeserver could lie and make it up). I'd rather keep the source of truth in the prev_events.

room.setLastMarkerEventIdProcessed(event.getId())
}
});
}

/**
Expand All @@ -250,6 +278,7 @@ export class SyncApi {
room.currentState.removeAllListeners(RoomStateEvent.Events);
room.currentState.removeAllListeners(RoomStateEvent.Members);
room.currentState.removeAllListeners(RoomStateEvent.NewMember);
room.currentState.removeAllListeners(RoomStateEvent.Marker);
}

/**
Expand Down