Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 6150b86

Browse files
author
Kerry
authored
Overlay virtual room call events into main timeline (#9626)
* super WIP POC for merging virtual room events into main timeline * remove some debugs * c * add some todos * remove hardcoded fake virtual user * insert overlay events into main timeline without resorting main tl events * remove more debugs * add extra tick to roomview tests * RoomView test case for virtual room * test case for merged timeline * make overlay event filter generic * remove TODOs from LegacyCallEventGrouper * tidy comments * remove some newlines * test timelinepanel room timeline event handling * use newState.roomId * fix strict errors in RoomView * fix strict errors in TimelinePanel * add type * pr tweaks * strict errors * more strict fix * strict error whackamole * update ROomView tests to use rtl
1 parent 1b6d753 commit 6150b86

File tree

7 files changed

+1174
-88
lines changed

7 files changed

+1174
-88
lines changed

src/VoipUserMapper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default class VoipUserMapper {
7979
return findDMForUser(MatrixClientPeg.get(), virtualUser);
8080
}
8181

82-
public nativeRoomForVirtualRoom(roomId: string): string {
82+
public nativeRoomForVirtualRoom(roomId: string): string | null {
8383
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
8484
if (cachedNativeRoomId) {
8585
logger.log(

src/components/structures/LegacyCallEventGrouper.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ export enum CustomCallState {
4444
Missed = "missed",
4545
}
4646

47+
const isCallEventType = (eventType: string): boolean =>
48+
eventType.startsWith("m.call.") || eventType.startsWith("org.matrix.call.");
49+
50+
export const isCallEvent = (event: MatrixEvent): boolean => isCallEventType(event.getType());
51+
4752
export function buildLegacyCallEventGroupers(
4853
callEventGroupers: Map<string, LegacyCallEventGrouper>,
4954
events?: MatrixEvent[],
5055
): Map<string, LegacyCallEventGrouper> {
5156
const newCallEventGroupers = new Map();
5257
events?.forEach(ev => {
53-
if (!ev.getType().startsWith("m.call.") && !ev.getType().startsWith("org.matrix.call.")) {
58+
if (!isCallEvent(ev)) {
5459
return;
5560
}
5661

src/components/structures/RoomView.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ import { CallStore, CallStoreEvent } from "../../stores/CallStore";
110110
import { Call } from "../../models/Call";
111111
import { RoomSearchView } from './RoomSearchView';
112112
import eventSearch from "../../Searching";
113+
import VoipUserMapper from '../../VoipUserMapper';
114+
import { isCallEvent } from './LegacyCallEventGrouper';
113115

114116
const DEBUG = false;
115117
let debuglog = function(msg: string) {};
@@ -144,6 +146,7 @@ enum MainSplitContentType {
144146
}
145147
export interface IRoomState {
146148
room?: Room;
149+
virtualRoom?: Room;
147150
roomId?: string;
148151
roomAlias?: string;
149152
roomLoading: boolean;
@@ -654,7 +657,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
654657
// NB: This does assume that the roomID will not change for the lifetime of
655658
// the RoomView instance
656659
if (initial) {
657-
newState.room = this.context.client.getRoom(newState.roomId);
660+
const virtualRoom = newState.roomId ?
661+
await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(newState.roomId) : undefined;
662+
663+
newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
664+
newState.virtualRoom = virtualRoom || undefined;
658665
if (newState.room) {
659666
newState.showApps = this.shouldShowApps(newState.room);
660667
this.onRoomLoaded(newState.room);
@@ -1264,7 +1271,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
12641271
});
12651272
}
12661273

1267-
private onRoom = (room: Room) => {
1274+
private onRoom = async (room: Room) => {
12681275
if (!room || room.roomId !== this.state.roomId) {
12691276
return;
12701277
}
@@ -1277,16 +1284,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
12771284
);
12781285
}
12791286

1287+
const virtualRoom = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(room.roomId);
12801288
this.setState({
12811289
room: room,
1290+
virtualRoom: virtualRoom || undefined,
12821291
}, () => {
12831292
this.onRoomLoaded(room);
12841293
});
12851294
};
12861295

12871296
private onDeviceVerificationChanged = (userId: string) => {
12881297
const room = this.state.room;
1289-
if (!room.currentState.getMember(userId)) {
1298+
if (!room?.currentState.getMember(userId)) {
12901299
return;
12911300
}
12921301
this.updateE2EStatus(room);
@@ -2093,7 +2102,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
20932102
hideMessagePanel = true;
20942103
}
20952104

2096-
let highlightedEventId = null;
2105+
let highlightedEventId: string | undefined;
20972106
if (this.state.isInitialEventHighlighted) {
20982107
highlightedEventId = this.state.initialEventId;
20992108
}
@@ -2102,6 +2111,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
21022111
<TimelinePanel
21032112
ref={this.gatherTimelinePanelRef}
21042113
timelineSet={this.state.room.getUnfilteredTimelineSet()}
2114+
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
2115+
overlayTimelineSetFilter={isCallEvent}
21052116
showReadReceipts={this.state.showReadReceipts}
21062117
manageReadReceipts={!this.state.isPeeking}
21072118
sendReadReceiptOnLoad={!this.state.wasContextSwitch}

src/components/structures/TimelinePanel.tsx

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ interface IProps {
7676
// a timeline representing. If it has a room, we maintain RRs etc for
7777
// that room.
7878
timelineSet: EventTimelineSet;
79+
// overlay events from a second timelineset on the main timeline
80+
// added to support virtual rooms
81+
// events from the overlay timeline set will be added by localTimestamp
82+
// into the main timeline
83+
// back paging not yet supported
84+
overlayTimelineSet?: EventTimelineSet;
85+
// filter events from overlay timeline
86+
overlayTimelineSetFilter?: (event: MatrixEvent) => boolean;
7987
showReadReceipts?: boolean;
8088
// Enable managing RRs and RMs. These require the timelineSet to have a room.
8189
manageReadReceipts?: boolean;
@@ -236,14 +244,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
236244
private readonly messagePanel = createRef<MessagePanel>();
237245
private readonly dispatcherRef: string;
238246
private timelineWindow?: TimelineWindow;
247+
private overlayTimelineWindow?: TimelineWindow;
239248
private unmounted = false;
240-
private readReceiptActivityTimer: Timer;
241-
private readMarkerActivityTimer: Timer;
249+
private readReceiptActivityTimer: Timer | null = null;
250+
private readMarkerActivityTimer: Timer | null = null;
242251

243252
// A map of <callId, LegacyCallEventGrouper>
244253
private callEventGroupers = new Map<string, LegacyCallEventGrouper>();
245254

246-
constructor(props, context) {
255+
constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
247256
super(props, context);
248257
this.context = context;
249258

@@ -642,7 +651,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
642651
data: IRoomTimelineData,
643652
): void => {
644653
// ignore events for other timeline sets
645-
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
654+
if (
655+
data.timeline.getTimelineSet() !== this.props.timelineSet
656+
&& data.timeline.getTimelineSet() !== this.props.overlayTimelineSet
657+
) {
658+
return;
659+
}
646660

647661
if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) {
648662
if (toStartOfTimeline && !this.state.canBackPaginate) {
@@ -680,21 +694,27 @@ class TimelinePanel extends React.Component<IProps, IState> {
680694
// timeline window.
681695
//
682696
// see https://github.com/vector-im/vector-web/issues/1035
683-
this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
684-
if (this.unmounted) { return; }
685-
686-
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
687-
this.buildLegacyCallEventGroupers(events);
688-
const lastLiveEvent = liveEvents[liveEvents.length - 1];
689-
690-
const updatedState: Partial<IState> = {
691-
events,
692-
liveEvents,
693-
firstVisibleEventIndex,
694-
};
697+
this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false)
698+
.then(() => {
699+
if (this.overlayTimelineWindow) {
700+
return this.overlayTimelineWindow.paginate(EventTimeline.FORWARDS, 1, false);
701+
}
702+
})
703+
.then(() => {
704+
if (this.unmounted) { return; }
705+
706+
const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
707+
this.buildLegacyCallEventGroupers(events);
708+
const lastLiveEvent = liveEvents[liveEvents.length - 1];
709+
710+
const updatedState: Partial<IState> = {
711+
events,
712+
liveEvents,
713+
firstVisibleEventIndex,
714+
};
695715

696-
let callRMUpdated;
697-
if (this.props.manageReadMarkers) {
716+
let callRMUpdated = false;
717+
if (this.props.manageReadMarkers) {
698718
// when a new event arrives when the user is not watching the
699719
// window, but the window is in its auto-scroll mode, make sure the
700720
// read marker is visible.
@@ -703,28 +723,28 @@ class TimelinePanel extends React.Component<IProps, IState> {
703723
// read-marker when a remote echo of an event we have just sent takes
704724
// more than the timeout on userActiveRecently.
705725
//
706-
const myUserId = MatrixClientPeg.get().credentials.userId;
707-
callRMUpdated = false;
708-
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
709-
updatedState.readMarkerVisible = true;
710-
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
726+
const myUserId = MatrixClientPeg.get().credentials.userId;
727+
callRMUpdated = false;
728+
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
729+
updatedState.readMarkerVisible = true;
730+
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
711731
// we know we're stuckAtBottom, so we can advance the RM
712732
// immediately, to save a later render cycle
713733

714-
this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
715-
updatedState.readMarkerVisible = false;
716-
updatedState.readMarkerEventId = lastLiveEvent.getId();
717-
callRMUpdated = true;
734+
this.setReadMarker(lastLiveEvent.getId() ?? null, lastLiveEvent.getTs(), true);
735+
updatedState.readMarkerVisible = false;
736+
updatedState.readMarkerEventId = lastLiveEvent.getId();
737+
callRMUpdated = true;
738+
}
718739
}
719-
}
720740

721-
this.setState<null>(updatedState, () => {
722-
this.messagePanel.current?.updateTimelineMinHeight();
723-
if (callRMUpdated) {
724-
this.props.onReadMarkerUpdated?.();
725-
}
741+
this.setState(updatedState as IState, () => {
742+
this.messagePanel.current?.updateTimelineMinHeight();
743+
if (callRMUpdated) {
744+
this.props.onReadMarkerUpdated?.();
745+
}
746+
});
726747
});
727-
});
728748
};
729749

730750
private onRoomTimelineReset = (room: Room, timelineSet: EventTimelineSet): void => {
@@ -735,7 +755,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
735755
}
736756
};
737757

738-
public canResetTimeline = () => this.messagePanel?.current.isAtBottom();
758+
public canResetTimeline = () => this.messagePanel?.current?.isAtBottom();
739759

740760
private onRoomRedaction = (ev: MatrixEvent, room: Room): void => {
741761
if (this.unmounted) return;
@@ -1337,6 +1357,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
13371357
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
13381358
const cli = MatrixClientPeg.get();
13391359
this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap });
1360+
this.overlayTimelineWindow = this.props.overlayTimelineSet
1361+
? new TimelineWindow(cli, this.props.overlayTimelineSet, { windowLimit: this.props.timelineCap })
1362+
: undefined;
13401363

13411364
const onLoaded = () => {
13421365
if (this.unmounted) return;
@@ -1351,8 +1374,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
13511374
this.advanceReadMarkerPastMyEvents();
13521375

13531376
this.setState({
1354-
canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS),
1355-
canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS),
1377+
canBackPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS),
1378+
canForwardPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS),
13561379
timelineLoading: false,
13571380
}, () => {
13581381
// initialise the scroll state of the message panel
@@ -1433,12 +1456,19 @@ class TimelinePanel extends React.Component<IProps, IState> {
14331456
// if we've got an eventId, and the timeline exists, we can skip
14341457
// the promise tick.
14351458
this.timelineWindow.load(eventId, INITIAL_SIZE);
1459+
this.overlayTimelineWindow?.load(undefined, INITIAL_SIZE);
14361460
// in this branch this method will happen in sync time
14371461
onLoaded();
14381462
return;
14391463
}
14401464

1441-
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
1465+
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async () => {
1466+
if (this.overlayTimelineWindow) {
1467+
// @TODO(kerrya) use timestampToEvent to load the overlay timeline
1468+
// with more correct position when main TL eventId is truthy
1469+
await this.overlayTimelineWindow.load(undefined, INITIAL_SIZE);
1470+
}
1471+
});
14421472
this.buildLegacyCallEventGroupers();
14431473
this.setState({
14441474
events: [],
@@ -1471,7 +1501,23 @@ class TimelinePanel extends React.Component<IProps, IState> {
14711501

14721502
// get the list of events from the timeline window and the pending event list
14731503
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
1474-
const events: MatrixEvent[] = this.timelineWindow.getEvents();
1504+
const mainEvents: MatrixEvent[] = this.timelineWindow?.getEvents() || [];
1505+
const eventFilter = this.props.overlayTimelineSetFilter || Boolean;
1506+
const overlayEvents = this.overlayTimelineWindow?.getEvents().filter(eventFilter) || [];
1507+
1508+
// maintain the main timeline event order as returned from the HS
1509+
// merge overlay events at approximately the right position based on local timestamp
1510+
const events = overlayEvents.reduce((acc: MatrixEvent[], overlayEvent: MatrixEvent) => {
1511+
// find the first main tl event with a later timestamp
1512+
const index = acc.findIndex(event => event.localTimestamp > overlayEvent.localTimestamp);
1513+
// insert overlay event into timeline at approximately the right place
1514+
if (index > -1) {
1515+
acc.splice(index, 0, overlayEvent);
1516+
} else {
1517+
acc.push(overlayEvent);
1518+
}
1519+
return acc;
1520+
}, [...mainEvents]);
14751521

14761522
// `arrayFastClone` performs a shallow copy of the array
14771523
// we want the last event to be decrypted first but displayed last
@@ -1483,20 +1529,20 @@ class TimelinePanel extends React.Component<IProps, IState> {
14831529
client.decryptEventIfNeeded(event);
14841530
});
14851531

1486-
const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
1532+
const firstVisibleEventIndex = this.checkForPreJoinUISI(mainEvents);
14871533

14881534
// Hold onto the live events separately. The read receipt and read marker
14891535
// should use this list, so that they don't advance into pending events.
14901536
const liveEvents = [...events];
14911537

14921538
// if we're at the end of the live timeline, append the pending events
1493-
if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
1539+
if (!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) {
14941540
const pendingEvents = this.props.timelineSet.getPendingEvents();
14951541
events.push(...pendingEvents.filter(event => {
14961542
const {
14971543
shouldLiveInRoom,
14981544
threadId,
1499-
} = this.props.timelineSet.room.eventShouldLiveIn(event, pendingEvents);
1545+
} = this.props.timelineSet.room!.eventShouldLiveIn(event, pendingEvents);
15001546

15011547
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
15021548
return threadId === this.context.threadId;

0 commit comments

Comments
 (0)