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

Overlay virtual room call events into main timeline #9626

Merged
merged 41 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8435994
super WIP POC for merging virtual room events into main timeline
Nov 24, 2022
fe7fcc5
remove some debugs
Nov 24, 2022
f6b9303
c
Nov 24, 2022
431347d
Merge branch 'develop' into psg-1020/combined-timeline
Nov 25, 2022
0dfb306
add some todos
Nov 25, 2022
4905371
remove hardcoded fake virtual user
Nov 25, 2022
660fbca
insert overlay events into main timeline without resorting main tl ev…
Nov 25, 2022
622aa62
remove more debugs
Nov 25, 2022
4facc7c
add extra tick to roomview tests
Nov 28, 2022
112471e
RoomView test case for virtual room
Nov 28, 2022
1b1a2e0
test case for merged timeline
Nov 28, 2022
ef0786d
make overlay event filter generic
Nov 28, 2022
28f039d
Merge branch 'develop' into psg-1020/combined-timeline
Nov 28, 2022
6653005
remove TODOs from LegacyCallEventGrouper
Nov 29, 2022
e831aa6
tidy comments
Nov 29, 2022
26b39d4
remove some newlines
Nov 29, 2022
103f399
test timelinepanel room timeline event handling
Nov 29, 2022
8712014
Merge branch 'develop' into psg-1020/combined-timeline
Nov 29, 2022
6ba8c89
Merge branch 'develop' into psg-1020/combined-timeline
Nov 29, 2022
eebd307
use newState.roomId
Nov 30, 2022
9309b53
Merge branch 'develop' into psg-1020/combined-timeline
Nov 30, 2022
49dc6a4
Merge branch 'develop' into psg-1020/combined-timeline
Dec 1, 2022
67adbd9
fix strict errors in RoomView
Dec 1, 2022
cbb06c2
fix strict errors in TimelinePanel
Dec 1, 2022
a2f17c8
Merge branch 'psg-1020/combined-timeline' of https://github.com/matri…
Dec 1, 2022
6e1cee7
Merge branch 'develop' into psg-1020/combined-timeline
Dec 2, 2022
463db5c
Merge branch 'develop' into psg-1020/combined-timeline
Dec 5, 2022
93f5d7a
add type
Dec 5, 2022
b8df60e
pr tweaks
Dec 5, 2022
772c8b3
strict errors
Dec 5, 2022
3ab91f0
more strict fix
Dec 5, 2022
e552274
strict error whackamole
Dec 5, 2022
458b1f1
Merge branch 'develop' into psg-1020/combined-timeline
Dec 6, 2022
da1dca4
Merge branch 'develop' into psg-1020/combined-timeline
Dec 7, 2022
8431820
Merge branch 'develop' into psg-1020/combined-timeline
Dec 7, 2022
6f480d8
update ROomView tests to use rtl
Dec 8, 2022
9b787ca
Merge branch 'develop' into psg-1020/combined-timeline
Dec 8, 2022
284d56e
more test coverage
Dec 8, 2022
cdd4f09
Merge branch 'psg-1020/combined-timeline' of https://github.com/matri…
Dec 8, 2022
8f35221
strict fixes in test
Dec 8, 2022
4d42a84
Merge branch 'develop' into psg-1020/combined-timeline
Dec 8, 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: 1 addition & 1 deletion src/VoipUserMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class VoipUserMapper {
return findDMForUser(MatrixClientPeg.get(), virtualUser);
}

public nativeRoomForVirtualRoom(roomId: string): string {
public nativeRoomForVirtualRoom(roomId: string): string | null {
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
if (cachedNativeRoomId) {
logger.log(
Expand Down
7 changes: 6 additions & 1 deletion src/components/structures/LegacyCallEventGrouper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,18 @@ export enum CustomCallState {
Missed = "missed",
}

const isCallEventType = (eventType: string): boolean =>
eventType.startsWith("m.call.") || eventType.startsWith("org.matrix.call.");

export const isCallEvent = (event: MatrixEvent): boolean => isCallEventType(event.getType());

export function buildLegacyCallEventGroupers(
callEventGroupers: Map<string, LegacyCallEventGrouper>,
events?: MatrixEvent[],
): Map<string, LegacyCallEventGrouper> {
const newCallEventGroupers = new Map();
events?.forEach(ev => {
if (!ev.getType().startsWith("m.call.") && !ev.getType().startsWith("org.matrix.call.")) {
if (!isCallEvent(ev)) {
return;
}

Expand Down
19 changes: 15 additions & 4 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { Call } from "../../models/Call";
import { RoomSearchView } from './RoomSearchView';
import eventSearch from "../../Searching";
import VoipUserMapper from '../../VoipUserMapper';
import { isCallEvent } from './LegacyCallEventGrouper';

const DEBUG = false;
let debuglog = function(msg: string) {};
Expand Down Expand Up @@ -144,6 +146,7 @@ enum MainSplitContentType {
}
export interface IRoomState {
room?: Room;
virtualRoom?: Room;
roomId?: string;
roomAlias?: string;
roomLoading: boolean;
Expand Down Expand Up @@ -654,7 +657,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = this.context.client.getRoom(newState.roomId);
const virtualRoom = newState.roomId ?
await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(newState.roomId) : undefined;

newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
newState.virtualRoom = virtualRoom || undefined;
if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room);
Expand Down Expand Up @@ -1264,7 +1271,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
}

private onRoom = (room: Room) => {
private onRoom = async (room: Room) => {
if (!room || room.roomId !== this.state.roomId) {
return;
}
Expand All @@ -1277,16 +1284,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
);
}

const virtualRoom = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(room.roomId);
this.setState({
room: room,
virtualRoom: virtualRoom || undefined,
}, () => {
this.onRoomLoaded(room);
});
};

private onDeviceVerificationChanged = (userId: string) => {
const room = this.state.room;
if (!room.currentState.getMember(userId)) {
if (!room?.currentState.getMember(userId)) {
return;
}
this.updateE2EStatus(room);
Expand Down Expand Up @@ -2093,7 +2102,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
hideMessagePanel = true;
}

let highlightedEventId = null;
let highlightedEventId: string | undefined;
if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.initialEventId;
}
Expand All @@ -2102,6 +2111,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
<TimelinePanel
ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
overlayTimelineSetFilter={isCallEvent}
showReadReceipts={this.state.showReadReceipts}
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
Expand Down
130 changes: 88 additions & 42 deletions src/components/structures/TimelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ interface IProps {
// a timeline representing. If it has a room, we maintain RRs etc for
// that room.
timelineSet: EventTimelineSet;
// overlay events from a second timelineset on the main timeline
// added to support virtual rooms
// events from the overlay timeline set will be added by localTimestamp
// into the main timeline
// back paging not yet supported
overlayTimelineSet?: EventTimelineSet;
// filter events from overlay timeline
overlayTimelineSetFilter?: (event: MatrixEvent) => boolean;
showReadReceipts?: boolean;
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts?: boolean;
Expand Down Expand Up @@ -236,14 +244,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
private readonly messagePanel = createRef<MessagePanel>();
private readonly dispatcherRef: string;
private timelineWindow?: TimelineWindow;
private overlayTimelineWindow?: TimelineWindow;
private unmounted = false;
private readReceiptActivityTimer: Timer;
private readMarkerActivityTimer: Timer;
private readReceiptActivityTimer: Timer | null = null;
private readMarkerActivityTimer: Timer | null = null;

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

constructor(props, context) {
constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
this.context = context;

Expand Down Expand Up @@ -642,7 +651,12 @@ class TimelinePanel extends React.Component<IProps, IState> {
data: IRoomTimelineData,
): void => {
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
if (
data.timeline.getTimelineSet() !== this.props.timelineSet
&& data.timeline.getTimelineSet() !== this.props.overlayTimelineSet
) {
return;
}

if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) {
if (toStartOfTimeline && !this.state.canBackPaginate) {
Expand Down Expand Up @@ -680,21 +694,27 @@ class TimelinePanel extends React.Component<IProps, IState> {
// timeline window.
//
// see https://github.com/vector-im/vector-web/issues/1035
this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }

const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
this.buildLegacyCallEventGroupers(events);
const lastLiveEvent = liveEvents[liveEvents.length - 1];

const updatedState: Partial<IState> = {
events,
liveEvents,
firstVisibleEventIndex,
};
this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false)
.then(() => {
if (this.overlayTimelineWindow) {
return this.overlayTimelineWindow.paginate(EventTimeline.FORWARDS, 1, false);
}
})
.then(() => {
if (this.unmounted) { return; }

const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
this.buildLegacyCallEventGroupers(events);
const lastLiveEvent = liveEvents[liveEvents.length - 1];

const updatedState: Partial<IState> = {
events,
liveEvents,
firstVisibleEventIndex,
};

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

this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastLiveEvent.getId();
callRMUpdated = true;
this.setReadMarker(lastLiveEvent.getId() ?? null, lastLiveEvent.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastLiveEvent.getId();
callRMUpdated = true;
}
}
}

this.setState<null>(updatedState, () => {
this.messagePanel.current?.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated?.();
}
this.setState(updatedState as IState, () => {
this.messagePanel.current?.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated?.();
}
});
});
});
};

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

public canResetTimeline = () => this.messagePanel?.current.isAtBottom();
public canResetTimeline = () => this.messagePanel?.current?.isAtBottom();

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

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

this.setState({
canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS),
canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS),
canBackPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS),
canForwardPaginate: !!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS),
timelineLoading: false,
}, () => {
// initialise the scroll state of the message panel
Expand Down Expand Up @@ -1433,12 +1456,19 @@ class TimelinePanel extends React.Component<IProps, IState> {
// if we've got an eventId, and the timeline exists, we can skip
// the promise tick.
this.timelineWindow.load(eventId, INITIAL_SIZE);
this.overlayTimelineWindow?.load(undefined, INITIAL_SIZE);
// in this branch this method will happen in sync time
onLoaded();
return;
}

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

// get the list of events from the timeline window and the pending event list
private getEvents(): Pick<IState, "events" | "liveEvents" | "firstVisibleEventIndex"> {
const events: MatrixEvent[] = this.timelineWindow.getEvents();
const mainEvents: MatrixEvent[] = this.timelineWindow?.getEvents() || [];
const eventFilter = this.props.overlayTimelineSetFilter || Boolean;
const overlayEvents = this.overlayTimelineWindow?.getEvents().filter(eventFilter) || [];

// maintain the main timeline event order as returned from the HS
// merge overlay events at approximately the right position based on local timestamp
const events = overlayEvents.reduce((acc: MatrixEvent[], overlayEvent: MatrixEvent) => {
// find the first main tl event with a later timestamp
const index = acc.findIndex(event => event.localTimestamp > overlayEvent.localTimestamp);
// insert overlay event into timeline at approximately the right place
if (index > -1) {
acc.splice(index, 0, overlayEvent);
} else {
acc.push(overlayEvent);
}
return acc;
}, [...mainEvents]);

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

const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
const firstVisibleEventIndex = this.checkForPreJoinUISI(mainEvents);

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

// if we're at the end of the live timeline, append the pending events
if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
if (!this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) {
const pendingEvents = this.props.timelineSet.getPendingEvents();
events.push(...pendingEvents.filter(event => {
const {
shouldLiveInRoom,
threadId,
} = this.props.timelineSet.room.eventShouldLiveIn(event, pendingEvents);
} = this.props.timelineSet.room!.eventShouldLiveIn(event, pendingEvents);

if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
return threadId === this.context.threadId;
Expand Down
14 changes: 14 additions & 0 deletions test/components/structures/RoomView-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { DirectoryMember } from "../../../src/utils/direct-messages";
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
import { SdkContextClass, SDKContext } from "../../../src/contexts/SDKContext";
import VoipUserMapper from "../../../src/VoipUserMapper";

const RoomView = wrapInMatrixClientContext(_RoomView);

Expand All @@ -67,6 +68,8 @@ describe("RoomView", () => {
stores = new SdkContextClass();
stores.client = cli;
stores.rightPanelStore.useUnitTestClient(cli);

jest.spyOn(VoipUserMapper.sharedInstance(), 'getVirtualRoomForRoom').mockResolvedValue(null);
});

afterEach(async () => {
Expand Down Expand Up @@ -108,6 +111,7 @@ describe("RoomView", () => {
</SDKContext.Provider>,
);
await act(() => Promise.resolve()); // Allow state to settle
roomView.setProps({});
Copy link
Member

Choose a reason for hiding this comment

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

Is this to force an update? That feels like a bit of an antipattern to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using rtl seems to solve the issue with the UI not updating

return roomView;
};
const getRoomViewInstance = async (): Promise<_RoomView> =>
Expand Down Expand Up @@ -155,6 +159,16 @@ describe("RoomView", () => {
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
});

describe('with virtual rooms', () => {
it("checks for a virtual room", async () => {
const roomView = await mountRoomView();
expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId);

// quick check that rendered without error
expect(roomView.find('.mx_ErrorBoundary').length).toBeFalsy();
});
});

describe("video rooms", () => {
beforeEach(async () => {
// Make it a video room
Expand Down
Loading