From aa821a5b6fbe291fafaf232849dd23e57a39a1e7 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 16 Apr 2025 09:36:34 +0100 Subject: [PATCH] Remove virtual rooms (#29635) * Remove virtual rooms from the timelinePanel and RoomView * Remove VoipUserMapper * Remove some unneeded imports * Remove tovirtual slash command test * Remove getSupportsVirtualRooms and virtualLookup * lint * Remove PROTOCOL_SIP_NATIVE * Remove native/virtual looks fields and fix tests * Remove unused lookup fields --- src/@types/global.d.ts | 2 - src/LegacyCallHandler.tsx | 104 +----- src/Notifier.ts | 11 +- src/SlashCommands.tsx | 23 -- src/VoipUserMapper.ts | 150 --------- src/call-types.ts | 4 - src/components/structures/RoomView.tsx | 12 - src/components/structures/TimelinePanel.tsx | 257 +++------------ src/createRoom.ts | 31 -- src/i18n/strings/en_EN.json | 2 - src/stores/room-list/RoomListStore.ts | 10 - .../room-list/filters/VisibilityProvider.ts | 13 - test/unit-tests/LegacyCallHandler-test.ts | 76 +---- test/unit-tests/SlashCommands-test.tsx | 41 --- .../components/structures/RoomView-test.tsx | 23 -- .../structures/TimelinePanel-test.tsx | 309 ------------------ .../__snapshots__/RoomView-test.tsx.snap | 4 +- .../filters/VisibilityProvider-test.ts | 43 --- 18 files changed, 68 insertions(+), 1047 deletions(-) delete mode 100644 src/VoipUserMapper.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 3bbeda067b..344059fee4 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -29,7 +29,6 @@ import type LegacyCallHandler from "../LegacyCallHandler"; import type UserActivity from "../UserActivity"; import { type ModalWidgetStore } from "../stores/ModalWidgetStore"; import { type WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; -import type VoipUserMapper from "../VoipUserMapper"; import { type SpaceStoreClass } from "../stores/spaces/SpaceStore"; import type TypingStore from "../stores/TypingStore"; import { type EventIndexPeg } from "../indexing/EventIndexPeg"; @@ -113,7 +112,6 @@ declare global { mxLegacyCallHandler: LegacyCallHandler; mxUserActivity: UserActivity; mxModalWidgetStore: ModalWidgetStore; - mxVoipUserMapper: VoipUserMapper; mxSpaceStore: SpaceStoreClass; mxVoiceRecordingStore: VoiceRecordingStore; mxTypingStore: TypingStore; diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 608d396dda..97cb478512 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -39,14 +39,12 @@ import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore"; import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; import { UIFeature } from "./settings/UIFeature"; import { Action } from "./dispatcher/actions"; -import VoipUserMapper from "./VoipUserMapper"; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid"; import SdkConfig from "./SdkConfig"; import { ensureDMExists } from "./createRoom"; import { Container, WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore"; import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast"; import ToastStore from "./stores/ToastStore"; -import Resend from "./Resend"; import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { InviteKind } from "./components/views/dialogs/InviteDialogTypes"; import { type OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload"; @@ -59,8 +57,6 @@ import { Jitsi } from "./widgets/Jitsi.ts"; export const PROTOCOL_PSTN = "m.protocol.pstn"; export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn"; -export const PROTOCOL_SIP_NATIVE = "im.vector.protocol.sip_native"; -export const PROTOCOL_SIP_VIRTUAL = "im.vector.protocol.sip_virtual"; const CHECK_PROTOCOLS_ATTEMPTS = 3; @@ -107,27 +103,9 @@ const debuglog = (...args: any[]): void => { } }; -interface ThirdpartyLookupResponseFields { - /* eslint-disable camelcase */ - - // im.vector.sip_native - virtual_mxid?: string; - is_virtual?: boolean; - - // im.vector.sip_virtual - native_mxid?: string; - is_native?: boolean; - - // common - lookup_success?: boolean; - - /* eslint-enable camelcase */ -} - interface ThirdpartyLookupResponse { userid: string; protocol: string; - fields: ThirdpartyLookupResponseFields; } export enum LegacyCallHandlerEvent { @@ -158,7 +136,6 @@ export default class LegacyCallHandler extends TypedEventEmitter(); // callId (target) -> call (transferee) private supportsPstnProtocol: boolean | null = null; private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol - private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native // Map of the asserted identity users after we've looked them up using the API. // We need to be be able to determine the mapped room synchronously, so we @@ -179,8 +156,7 @@ export default class LegacyCallHandler extends TypedEventEmitter { try { return await MatrixClientPeg.safeGet().getThirdpartyUser( @@ -323,28 +289,6 @@ export default class LegacyCallHandler extends TypedEventEmitter { - try { - return await MatrixClientPeg.safeGet().getThirdpartyUser(PROTOCOL_SIP_VIRTUAL, { - native_mxid: nativeMxid, - }); - } catch (e) { - logger.warn("Failed to query SIP identity for user", e); - return Promise.resolve([]); - } - } - - public async sipNativeLookup(virtualMxid: string): Promise { - try { - return await MatrixClientPeg.safeGet().getThirdpartyUser(PROTOCOL_SIP_NATIVE, { - virtual_mxid: virtualMxid, - }); - } catch (e) { - logger.warn("Failed to query identity for SIP user", e); - return Promise.resolve([]); - } - } - private onCallIncoming = (call: MatrixCall): void => { // if the runtime env doesn't do VoIP, stop here. if (!MatrixClientPeg.get()?.supportsVoip()) { @@ -537,24 +481,16 @@ export default class LegacyCallHandler extends TypedEventEmitter { const cli = MatrixClientPeg.safeGet(); - const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId; - logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); - - // If we're using a virtual room nd there are any events pending, try to resend them, - // otherwise the call will fail and because its a virtual room, the user won't be able - // to see it to either retry or clear the pending events. There will only be call events - // in this queue, and since we're about to place a new call, they can only be events from - // previous calls that are probably stale by now, so just cancel them. - if (mappedRoomId !== roomId) { - const mappedRoom = cli.getRoom(mappedRoomId); - if (mappedRoom?.getPendingEvents().length) { - Resend.cancelUnsentEvents(mappedRoom); - } - } const timeUntilTurnCresExpire = cli.getTurnServersExpiry() - Date.now(); logger.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); - const call = cli.createCall(mappedRoomId)!; + const call = cli.createCall(roomId)!; try { this.addCallForRoom(roomId, call); @@ -978,19 +900,7 @@ export default class LegacyCallHandler extends TypedEventEmitter 0 && nativeLookupResults[0].fields.lookup_success; - nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; - logger.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); - } else { - nativeUserId = userId; - } - - const roomId = await ensureDMExists(MatrixClientPeg.safeGet(), nativeUserId); + const roomId = await ensureDMExists(MatrixClientPeg.safeGet(), userId); if (!roomId) { throw new Error("Failed to ensure DM exists for dialing number"); } diff --git a/src/Notifier.ts b/src/Notifier.ts index b24e146fbc..7dce26d6bd 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -43,8 +43,6 @@ import { isPushNotifyDisabled } from "./settings/controllers/NotificationControl import UserActivity from "./UserActivity"; import { mediaFromMxc } from "./customisations/Media"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; -import LegacyCallHandler from "./LegacyCallHandler"; -import VoipUserMapper from "./VoipUserMapper"; import { SdkContextClass } from "./contexts/SDKContext"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; @@ -447,14 +445,7 @@ class NotifierClass extends TypedEventEmitter { - return success( - (async (): Promise => { - const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId); - if (!room) throw new UserFriendlyError("slash_command|tovirtual_not_found"); - dis.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "SlashCommand", - metricsViaKeyboard: true, - }); - })(), - ); - }, - }), new Command({ command: "query", description: _td("slash_command|query"), diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts deleted file mode 100644 index c2a7810d96..0000000000 --- a/src/VoipUserMapper.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -import { type Room, EventType } from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; -import { logger } from "matrix-js-sdk/src/logger"; - -import { ensureVirtualRoomExists } from "./createRoom"; -import { MatrixClientPeg } from "./MatrixClientPeg"; -import DMRoomMap from "./utils/DMRoomMap"; -import LegacyCallHandler from "./LegacyCallHandler"; -import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types"; -import { findDMForUser } from "./utils/dm/findDMForUser"; - -// Functions for mapping virtual users & rooms. Currently the only lookup -// is sip virtual: there could be others in the future. - -export default class VoipUserMapper { - // We store mappings of virtual -> native room IDs here until the local echo for the - // account data arrives. - private virtualToNativeRoomIdCache = new Map(); - - public static sharedInstance(): VoipUserMapper { - if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); - return window.mxVoipUserMapper; - } - - private async userToVirtualUser(userId: string): Promise { - const results = await LegacyCallHandler.instance.sipVirtualLookup(userId); - if (results.length === 0 || !results[0].fields.lookup_success) return null; - return results[0].userid; - } - - private async getVirtualUserForRoom(roomId: string): Promise { - const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!userId) return null; - - const virtualUser = await this.userToVirtualUser(userId); - if (!virtualUser) return null; - - return virtualUser; - } - - public async getOrCreateVirtualRoomForRoom(roomId: string): Promise { - const virtualUser = await this.getVirtualUserForRoom(roomId); - if (!virtualUser) return null; - - const cli = MatrixClientPeg.safeGet(); - const virtualRoomId = await ensureVirtualRoomExists(cli, virtualUser, roomId); - cli.setRoomAccountData(virtualRoomId!, VIRTUAL_ROOM_EVENT_TYPE, { - native_room: roomId, - }); - - this.virtualToNativeRoomIdCache.set(virtualRoomId!, roomId); - - return virtualRoomId; - } - - /** - * Gets the ID of the virtual room for a room, or null if the room has no - * virtual room - */ - public async getVirtualRoomForRoom(roomId: string): Promise { - const virtualUser = await this.getVirtualUserForRoom(roomId); - if (!virtualUser) return undefined; - - return findDMForUser(MatrixClientPeg.safeGet(), virtualUser); - } - - public nativeRoomForVirtualRoom(roomId: string): string | null { - const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); - if (cachedNativeRoomId) { - logger.log( - "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache", - ); - return cachedNativeRoomId; - } - - const cli = MatrixClientPeg.safeGet(); - const virtualRoom = cli.getRoom(roomId); - if (!virtualRoom) return null; - const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); - if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; - const nativeRoomID = virtualRoomEvent.getContent()["native_room"]; - const nativeRoom = cli.getRoom(nativeRoomID); - if (!nativeRoom || nativeRoom.getMyMembership() !== KnownMembership.Join) return null; - - return nativeRoomID; - } - - public isVirtualRoom(room: Room): boolean { - if (this.nativeRoomForVirtualRoom(room.roomId)) return true; - - if (this.virtualToNativeRoomIdCache.has(room.roomId)) return true; - - // also look in the create event for the claimed native room ID, which is the only - // way we can recognise a virtual room we've created when it first arrives down - // our stream. We don't trust this in general though, as it could be faked by an - // inviter: our main source of truth is the DM state. - const roomCreateEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); - if (!roomCreateEvent || !roomCreateEvent.getContent()) return false; - // we only look at this for rooms we created (so inviters can't just cause rooms - // to be invisible) - if (roomCreateEvent.getSender() !== MatrixClientPeg.safeGet().getUserId()) return false; - const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE]; - return Boolean(claimedNativeRoomId); - } - - public async onNewInvitedRoom(invitedRoom: Room): Promise { - if (!LegacyCallHandler.instance.getSupportsVirtualRooms()) return; - - const inviterId = invitedRoom.getDMInviter(); - if (!inviterId) { - logger.error("Could not find DM inviter for room id: " + invitedRoom.roomId); - } - - logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); - const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId!); - if (result.length === 0) { - return; - } - - if (result[0].fields.is_virtual) { - const cli = MatrixClientPeg.safeGet(); - const nativeUser = result[0].userid; - const nativeRoom = findDMForUser(cli, nativeUser); - if (nativeRoom) { - // It's a virtual room with a matching native room, so set the room account data. This - // will make sure we know where how to map calls and also allow us know not to display - // it in the future. - cli.setRoomAccountData(invitedRoom.roomId, VIRTUAL_ROOM_EVENT_TYPE, { - native_room: nativeRoom.roomId, - }); - // also auto-join the virtual room if we have a matching native room - // (possibly we should only join if we've also joined the native room, then we'd also have - // to make sure we joined virtual rooms on joining a native one) - cli.joinRoom(invitedRoom.roomId); - - // also put this room in the virtual room ID cache so isVirtualRoom return the right answer - // in however long it takes for the echo of setAccountData to come down the sync - this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); - } - } - } -} diff --git a/src/call-types.ts b/src/call-types.ts index 2a263174ef..6586bcf3b9 100644 --- a/src/call-types.ts +++ b/src/call-types.ts @@ -6,10 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -// Event type for room account data and room creation content used to mark rooms as virtual rooms -// (and store the ID of their native room) -export const VIRTUAL_ROOM_EVENT_TYPE = "im.vector.is_virtual_room"; - export const JitsiCallMemberEventType = "io.element.video.member"; export interface JitsiCallMemberContent { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 7ad4abe15d..0dbc6aaf3f 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -120,8 +120,6 @@ import { isVideoRoom } from "../../utils/video-rooms"; import { SDKContext } from "../../contexts/SDKContext"; import { RoomSearchView } from "./RoomSearchView"; import eventSearch, { type SearchInfo, SearchScope } from "../../Searching"; -import VoipUserMapper from "../../VoipUserMapper"; -import { isCallEvent } from "./LegacyCallEventGrouper"; import { WidgetType } from "../../widgets/WidgetType"; import WidgetUtils from "../../utils/WidgetUtils"; import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite"; @@ -165,7 +163,6 @@ export { MainSplitContentType }; export interface IRoomState { room?: Room; - virtualRoom?: Room; roomId?: string; roomAlias?: string; roomLoading: boolean; @@ -1344,12 +1341,6 @@ export class RoomView extends React.Component { return this.messagePanel.canResetTimeline(); }; - private loadVirtualRoom = async (room?: Room): Promise => { - const virtualRoom = room?.roomId && (await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(room?.roomId)); - - this.setState({ virtualRoom: virtualRoom || undefined }); - }; - // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). private onRoomLoaded = (room: Room): void => { @@ -1362,7 +1353,6 @@ export class RoomView extends React.Component { this.calculateRecommendedVersion(room); this.updatePermissions(room); this.checkWidgets(room); - this.loadVirtualRoom(room); this.updateRoomEncrypted(room); if ( @@ -2444,8 +2434,6 @@ export class RoomView extends React.Component { { } }; -const overlaysBefore = (overlayEvent: MatrixEvent, mainEvent: MatrixEvent): boolean => - overlayEvent.localTimestamp < mainEvent.localTimestamp; - -const overlaysAfter = (overlayEvent: MatrixEvent, mainEvent: MatrixEvent): boolean => - overlayEvent.localTimestamp >= mainEvent.localTimestamp; - interface IProps { // The js-sdk EventTimelineSet object for the timeline sequence we are // representing. This may or may not have a room, depending on what it's // 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 - 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; @@ -251,7 +238,6 @@ class TimelinePanel extends React.Component { private readonly messagePanel = createRef(); private dispatcherRef?: string; private timelineWindow?: TimelineWindow; - private overlayTimelineWindow?: TimelineWindow; private unmounted = false; private readReceiptActivityTimer: Timer | null = null; private readMarkerActivityTimer: Timer | null = null; @@ -349,16 +335,12 @@ class TimelinePanel extends React.Component { const differentEventId = prevProps.eventId != this.props.eventId; const differentHighlightedEventId = prevProps.highlightedEventId != this.props.highlightedEventId; const differentAvoidJump = prevProps.eventScrollIntoView && !this.props.eventScrollIntoView; - const differentOverlayTimeline = prevProps.overlayTimelineSet !== this.props.overlayTimelineSet; if (differentEventId || differentHighlightedEventId || differentAvoidJump) { logger.log( `TimelinePanel switching to eventId ${this.props.eventId} (was ${prevProps.eventId}), ` + `scrollIntoView: ${this.props.eventScrollIntoView} (was ${prevProps.eventScrollIntoView})`, ); this.initTimeline(this.props); - } else if (differentOverlayTimeline) { - logger.log(`TimelinePanel updating overlay timeline.`); - this.initTimeline(this.props); } } @@ -509,24 +491,9 @@ class TimelinePanel extends React.Component { // this particular event should be the first or last to be unpaginated. const eventId = scrollToken; - // The event in question could belong to either the main timeline or - // overlay timeline; let's check both const mainEvents = this.timelineWindow!.getEvents(); - const overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; - let marker = mainEvents.findIndex((ev) => ev.getId() === eventId); - let overlayMarker: number; - if (marker === -1) { - // The event must be from the overlay timeline instead - overlayMarker = overlayEvents.findIndex((ev) => ev.getId() === eventId); - marker = backwards - ? findLastIndex(mainEvents, (ev) => overlaysAfter(overlayEvents[overlayMarker], ev)) - : mainEvents.findIndex((ev) => overlaysBefore(overlayEvents[overlayMarker], ev)); - } else { - overlayMarker = backwards - ? findLastIndex(overlayEvents, (ev) => overlaysBefore(ev, mainEvents[marker])) - : overlayEvents.findIndex((ev) => overlaysAfter(ev, mainEvents[marker])); - } + const marker = mainEvents.findIndex((ev) => ev.getId() === eventId); // The number of events to unpaginate from the main timeline let count: number; @@ -536,24 +503,11 @@ class TimelinePanel extends React.Component { count = backwards ? marker + 1 : mainEvents.length - marker; } - // The number of events to unpaginate from the overlay timeline - let overlayCount: number; - if (overlayMarker === -1) { - overlayCount = 0; - } else { - overlayCount = backwards ? overlayMarker + 1 : overlayEvents.length - overlayMarker; - } - if (count > 0) { debuglog("Unpaginating", count, "in direction", dir); this.timelineWindow!.unpaginate(count, backwards); } - if (overlayCount > 0) { - debuglog("Unpaginating", count, "from overlay timeline in direction", dir); - this.overlayTimelineWindow!.unpaginate(overlayCount, backwards); - } - const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); this.setState({ @@ -610,10 +564,6 @@ class TimelinePanel extends React.Component { return false; } - if (this.overlayTimelineWindow) { - await this.extendOverlayWindowToCoverMainWindow(); - } - debuglog("paginate complete backwards:" + backwards + "; success:" + r); const { events, liveEvents } = this.getEvents(); @@ -705,10 +655,7 @@ class TimelinePanel extends React.Component { data: IRoomTimelineData, ): void => { // ignore events for other timeline sets - if ( - data.timeline.getTimelineSet() !== this.props.timelineSet && - data.timeline.getTimelineSet() !== this.props.overlayTimelineSet - ) { + if (data.timeline.getTimelineSet() !== this.props.timelineSet) { return; } @@ -748,69 +695,60 @@ class TimelinePanel extends React.Component { // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false) - .then(() => { - if (this.overlayTimelineWindow) { - return this.overlayTimelineWindow.paginate(EventTimeline.FORWARDS, 1, false); + this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false).then(() => { + if (this.unmounted) { + return; + } + + const { events, liveEvents } = this.getEvents(); + this.buildLegacyCallEventGroupers(events); + const lastLiveEvent = liveEvents[liveEvents.length - 1]; + + const updatedState: Partial = { + events, + liveEvents, + }; + + 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. + // + // We ignore events we have sent ourselves; we don't want to see the + // read-marker when a remote echo of an event we have just sent takes + // more than the timeout on userActiveRecently. + // + const myUserId = MatrixClientPeg.safeGet().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() ?? null, lastLiveEvent.getTs(), true); + updatedState.readMarkerVisible = false; + updatedState.readMarkerEventId = lastLiveEvent.getId(); + callRMUpdated = true; } - }) - .then(() => { - if (this.unmounted) { - return; + } + + this.setState(updatedState as IState, () => { + this.messagePanel.current?.updateTimelineMinHeight(); + if (callRMUpdated) { + this.props.onReadMarkerUpdated?.(); } - - const { events, liveEvents } = this.getEvents(); - this.buildLegacyCallEventGroupers(events); - const lastLiveEvent = liveEvents[liveEvents.length - 1]; - - const updatedState: Partial = { - events, - liveEvents, - }; - - 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. - // - // We ignore events we have sent ourselves; we don't want to see the - // read-marker when a remote echo of an event we have just sent takes - // more than the timeout on userActiveRecently. - // - const myUserId = MatrixClientPeg.safeGet().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() ?? null, lastLiveEvent.getTs(), true); - updatedState.readMarkerVisible = false; - updatedState.readMarkerEventId = lastLiveEvent.getId(); - callRMUpdated = true; - } - } - - this.setState(updatedState as IState, () => { - this.messagePanel.current?.updateTimelineMinHeight(); - if (callRMUpdated) { - this.props.onReadMarkerUpdated?.(); - } - }); }); + }); }; private hasTimelineSetFor(roomId: string | undefined): boolean { - return ( - (roomId !== undefined && roomId === this.props.timelineSet.room?.roomId) || - roomId === this.props.overlayTimelineSet?.room?.roomId - ); + return roomId !== undefined && roomId === this.props.timelineSet.room?.roomId; } private onRoomTimelineReset = (room: Room | undefined, timelineSet: EventTimelineSet): void => { - if (timelineSet !== this.props.timelineSet && timelineSet !== this.props.overlayTimelineSet) return; + if (timelineSet !== this.props.timelineSet) return; if (this.canResetTimeline()) { this.loadTimeline(); @@ -1475,48 +1413,6 @@ class TimelinePanel extends React.Component { }); } - private async extendOverlayWindowToCoverMainWindow(): Promise { - const mainWindow = this.timelineWindow!; - const overlayWindow = this.overlayTimelineWindow!; - const mainEvents = mainWindow.getEvents(); - - if (mainEvents.length > 0) { - let paginationRequests: Promise[]; - - // Keep paginating until the main window is covered - do { - paginationRequests = []; - const overlayEvents = overlayWindow.getEvents(); - - if ( - overlayWindow.canPaginate(EventTimeline.BACKWARDS) && - (overlayEvents.length === 0 || - overlaysAfter(overlayEvents[0], mainEvents[0]) || - !mainWindow.canPaginate(EventTimeline.BACKWARDS)) - ) { - // Paginating backwards could reveal more events to be overlaid in the main window - paginationRequests.push( - this.onPaginationRequest(overlayWindow, EventTimeline.BACKWARDS, PAGINATE_SIZE), - ); - } - - if ( - overlayWindow.canPaginate(EventTimeline.FORWARDS) && - (overlayEvents.length === 0 || - overlaysBefore(overlayEvents.at(-1)!, mainEvents.at(-1)!) || - !mainWindow.canPaginate(EventTimeline.FORWARDS)) - ) { - // Paginating forwards could reveal more events to be overlaid in the main window - paginationRequests.push( - this.onPaginationRequest(overlayWindow, EventTimeline.FORWARDS, PAGINATE_SIZE), - ); - } - - await Promise.all(paginationRequests); - } while (paginationRequests.length > 0); - } - } - /** * (re)-load the event timeline, and initialise the scroll state, centered * around the given event. @@ -1536,9 +1432,6 @@ class TimelinePanel extends React.Component { private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void { const cli = MatrixClientPeg.safeGet(); 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 = (): void => { if (this.unmounted) return; @@ -1554,14 +1447,8 @@ class TimelinePanel extends React.Component { this.setState( { - canBackPaginate: - (this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS) || - this.overlayTimelineWindow?.canPaginate(EventTimeline.BACKWARDS)) ?? - false, - canForwardPaginate: - (this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) || - this.overlayTimelineWindow?.canPaginate(EventTimeline.FORWARDS)) ?? - false, + canBackPaginate: this.timelineWindow?.canPaginate(EventTimeline.BACKWARDS) ?? false, + canForwardPaginate: this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) ?? false, timelineLoading: false, }, () => { @@ -1636,7 +1523,7 @@ class TimelinePanel extends React.Component { // This is a hot-path optimization by skipping a promise tick // by repeating a no-op sync branch in // TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline - if (this.props.timelineSet.getTimelineForEvent(eventId) && !this.overlayTimelineWindow) { + if (this.props.timelineSet.getTimelineForEvent(eventId)) { // if we've got an eventId, and the timeline exists, we can skip // the promise tick. this.timelineWindow.load(eventId, INITIAL_SIZE); @@ -1645,14 +1532,7 @@ class TimelinePanel extends React.Component { return; } - const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise => { - if (this.overlayTimelineWindow) { - // TODO: use timestampToEvent to load the overlay timeline - // with more correct position when main TL eventId is truthy - await this.overlayTimelineWindow.load(undefined, INITIAL_SIZE); - await this.extendOverlayWindowToCoverMainWindow(); - } - }); + const prom = this.timelineWindow.load(eventId, INITIAL_SIZE); this.buildLegacyCallEventGroupers(); this.setState({ events: [], @@ -1683,38 +1563,9 @@ class TimelinePanel extends React.Component { this.reloadEvents(); } - // get the list of events from the timeline windows and the pending event list + // get the list of events from the timeline window and the pending event list private getEvents(): Pick { - const mainEvents = this.timelineWindow!.getEvents(); - let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; - if (this.props.overlayTimelineSetFilter !== undefined) { - overlayEvents = overlayEvents.filter(this.props.overlayTimelineSetFilter); - } - - // 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) => overlaysBefore(overlayEvent, event)); - // insert overlay event into timeline at approximately the right place - // if it's beyond the edge of the main window, hide it so that expanding - // the main window doesn't cause new events to pop in and change its position - if (index === -1) { - if (!this.timelineWindow!.canPaginate(EventTimeline.FORWARDS)) { - acc.push(overlayEvent); - } - } else if (index === 0) { - if (!this.timelineWindow!.canPaginate(EventTimeline.BACKWARDS)) { - acc.unshift(overlayEvent); - } - } else { - acc.splice(index, 0, overlayEvent); - } - return acc; - }, - [...mainEvents], - ); + const events = this.timelineWindow!.getEvents(); // We want the last event to be decrypted first const client = MatrixClientPeg.safeGet(); diff --git a/src/createRoom.ts b/src/createRoom.ts index f226f68011..df0f90b91f 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -28,7 +28,6 @@ import { _t, UserFriendlyError } from "./languageHandler"; import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import { getAddressType } from "./UserAddress"; -import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types"; import SpaceStore from "./stores/spaces/SpaceStore"; import { makeSpaceParentEvent } from "./utils/space"; import { JitsiCall, ElementCall } from "./models/Call"; @@ -423,36 +422,6 @@ export async function canEncryptToAllUsers(client: MatrixClient, userIds: string return true; } -// Similar to ensureDMExists but also adds creation content -// without polluting ensureDMExists with unrelated stuff (also -// they're never encrypted). -export async function ensureVirtualRoomExists( - client: MatrixClient, - userId: string, - nativeRoomId: string, -): Promise { - const existingDMRoom = findDMForUser(client, userId); - let roomId: string | null; - if (existingDMRoom) { - roomId = existingDMRoom.roomId; - } else { - roomId = await createRoom(client, { - dmUserId: userId, - spinner: false, - andView: false, - createOpts: { - creation_content: { - // This allows us to recognise that the room is a virtual room - // when it comes down our sync stream (we also put the ID of the - // respective native room in there because why not?) - [VIRTUAL_ROOM_EVENT_TYPE]: nativeRoomId, - }, - }, - }); - } - return roomId; -} - export async function ensureDMExists(client: MatrixClient, userId: string): Promise { const existingDMRoom = findDMForUser(client, userId); let roomId: string | null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 37a3a0718e..1fb2294eb6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3093,8 +3093,6 @@ "topic": "Gets or sets the room topic", "topic_none": "This room has no topic.", "topic_room_error": "Failed to get room topic: Unable to find room (%(roomId)s", - "tovirtual": "Switches to this room's virtual room, if it has one", - "tovirtual_not_found": "No virtual room for this room", "unban": "Unbans user with given ID", "unflip": "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message", "unholdcall": "Takes the call in the current room off hold", diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 6985e007bd..2fc396b8b3 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ import { type MatrixClient, type Room, type RoomState, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -349,15 +348,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient implem } private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - if (cause === RoomUpdateCause.NewRoom && room.getMyMembership() === KnownMembership.Invite) { - // Let the visibility provider know that there is a new invited room. It would be nice - // if this could just be an event that things listen for but the point of this is that - // we delay doing anything about this room until the VoipUserMapper had had a chance - // to do the things it needs to do to decide if we should show this room or not, so - // an even wouldn't et us do that. - await VisibilityProvider.instance.onNewInvitedRoom(room); - } - if (!VisibilityProvider.instance.isRoomVisible(room)) { return; // don't do anything on rooms that aren't visible } diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index f6b0acb030..178a1ed553 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -8,10 +8,8 @@ import { type Room } from "matrix-js-sdk/src/matrix"; -import LegacyCallHandler from "../../../LegacyCallHandler"; import { RoomListCustomisations } from "../../../customisations/RoomList"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; -import VoipUserMapper from "../../../VoipUserMapper"; export class VisibilityProvider { private static internalInstance: VisibilityProvider; @@ -25,22 +23,11 @@ export class VisibilityProvider { return VisibilityProvider.internalInstance; } - public async onNewInvitedRoom(room: Room): Promise { - await VoipUserMapper.sharedInstance().onNewInvitedRoom(room); - } - public isRoomVisible(room?: Room): boolean { if (!room) { return false; } - if ( - LegacyCallHandler.instance.getSupportsVirtualRooms() && - VoipUserMapper.sharedInstance().isVirtualRoom(room) - ) { - return false; - } - // hide space rooms as they'll be shown in the SpacePanel if (room.isSpaceRoom()) { return false; diff --git a/test/unit-tests/LegacyCallHandler-test.ts b/test/unit-tests/LegacyCallHandler-test.ts index 664e091393..972f17b7e0 100644 --- a/test/unit-tests/LegacyCallHandler-test.ts +++ b/test/unit-tests/LegacyCallHandler-test.ts @@ -29,8 +29,6 @@ import LegacyCallHandler, { LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, - PROTOCOL_SIP_NATIVE, - PROTOCOL_SIP_VIRTUAL, } from "../../src/LegacyCallHandler"; import { mkStubRoom, stubClient, untilDispatch } from "../test-utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; @@ -75,9 +73,6 @@ const NATIVE_ALICE = "@alice:example.org"; const NATIVE_BOB = "@bob:example.org"; const NATIVE_CHARLIE = "@charlie:example.org"; -// Virtual user for Bob -const VIRTUAL_BOB = "@virtual_bob:example.org"; - //const REAL_ROOM_ID = "$room1:example.org"; // The rooms the user sees when they're communicating with these users const NATIVE_ROOM_ALICE = "$alice_room:example.org"; @@ -86,10 +81,6 @@ const NATIVE_ROOM_CHARLIE = "$charlie_room:example.org"; const FUNCTIONAL_USER = "@bot:example.com"; -// The room we use to talk to virtual Bob (but that the user does not see) -// Bob has a virtual room, but Alice doesn't -const VIRTUAL_ROOM_BOB = "$virtual_bob_room:example.org"; - // Bob's phone number const BOB_PHONE_NUMBER = "01818118181"; @@ -146,14 +137,6 @@ class FakeCall extends EventEmitter { } } -function untilCallHandlerEvent(callHandler: LegacyCallHandler, event: LegacyCallHandlerEvent): Promise { - return new Promise((resolve) => { - callHandler.addListener(event, () => { - resolve(); - }); - }); -} - describe("LegacyCallHandler", () => { let dmRoomMap; let callHandler: LegacyCallHandler; @@ -162,7 +145,6 @@ describe("LegacyCallHandler", () => { // what addresses the app has looked up via pstn and native lookup let pstnLookup: string | null; - let nativeLookup: string | null; const deviceId = "my-device"; beforeEach(async () => { @@ -180,8 +162,6 @@ describe("LegacyCallHandler", () => { MatrixClientPeg.safeGet().getThirdpartyProtocols = () => { return Promise.resolve({ "m.id.phone": {} as IProtocol, - "im.vector.protocol.sip_native": {} as IProtocol, - "im.vector.protocol.sip_virtual": {} as IProtocol, }); }; @@ -193,7 +173,6 @@ describe("LegacyCallHandler", () => { const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE); const nativeRoomBob = mkStubDM(NATIVE_ROOM_BOB, NATIVE_BOB); const nativeRoomCharie = mkStubDM(NATIVE_ROOM_CHARLIE, NATIVE_CHARLIE); - const virtualBobRoom = mkStubDM(VIRTUAL_ROOM_BOB, VIRTUAL_BOB); MatrixClientPeg.safeGet().getRoom = (roomId: string): Room | null => { switch (roomId) { @@ -203,8 +182,6 @@ describe("LegacyCallHandler", () => { return nativeRoomBob; case NATIVE_ROOM_CHARLIE: return nativeRoomCharie; - case VIRTUAL_ROOM_BOB: - return virtualBobRoom; } return null; @@ -218,8 +195,6 @@ describe("LegacyCallHandler", () => { return NATIVE_BOB; } else if (roomId === NATIVE_ROOM_CHARLIE) { return NATIVE_CHARLIE; - } else if (roomId === VIRTUAL_ROOM_BOB) { - return VIRTUAL_BOB; } else { return null; } @@ -231,8 +206,6 @@ describe("LegacyCallHandler", () => { return [NATIVE_ROOM_BOB]; } else if (userId === NATIVE_CHARLIE) { return [NATIVE_ROOM_CHARLIE]; - } else if (userId === VIRTUAL_BOB) { - return [VIRTUAL_ROOM_BOB]; } else { return []; } @@ -241,52 +214,18 @@ describe("LegacyCallHandler", () => { DMRoomMap.setShared(dmRoomMap); pstnLookup = null; - nativeLookup = null; MatrixClientPeg.safeGet().getThirdpartyUser = (proto: string, params: any) => { if ([PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED].includes(proto)) { pstnLookup = params["m.id.phone"]; return Promise.resolve([ { - userid: VIRTUAL_BOB, + userid: NATIVE_BOB, protocol: "m.id.phone", - fields: { - is_native: true, - lookup_success: true, - }, + fields: {}, }, ]); - } else if (proto === PROTOCOL_SIP_NATIVE) { - nativeLookup = params["virtual_mxid"]; - if (params["virtual_mxid"] === VIRTUAL_BOB) { - return Promise.resolve([ - { - userid: NATIVE_BOB, - protocol: "im.vector.protocol.sip_native", - fields: { - is_native: true, - lookup_success: true, - }, - }, - ]); - } - return Promise.resolve([]); - } else if (proto === PROTOCOL_SIP_VIRTUAL) { - if (params["native_mxid"] === NATIVE_BOB) { - return Promise.resolve([ - { - userid: VIRTUAL_BOB, - protocol: "im.vector.protocol.sip_virtual", - fields: { - is_virtual: true, - lookup_success: true, - }, - }, - ]); - } - return Promise.resolve([]); } - return Promise.resolve([]); }; @@ -312,16 +251,14 @@ describe("LegacyCallHandler", () => { await callHandler.dialNumber(BOB_PHONE_NUMBER); expect(pstnLookup).toEqual(BOB_PHONE_NUMBER); - expect(nativeLookup).toEqual(VIRTUAL_BOB); // we should have switched to the native room for Bob const viewRoomPayload = await untilDispatch(Action.ViewRoom); expect(viewRoomPayload.room_id).toEqual(NATIVE_ROOM_BOB); // Check that a call was started: its room on the protocol level - // should be the virtual room expect(fakeCall).not.toBeNull(); - expect(fakeCall?.roomId).toEqual(VIRTUAL_ROOM_BOB); + expect(fakeCall?.roomId).toEqual(NATIVE_ROOM_BOB); // but it should appear to the user to be in thw native room for Bob expect(callHandler.roomIdForCall(fakeCall!)).toEqual(NATIVE_ROOM_BOB); @@ -338,7 +275,7 @@ describe("LegacyCallHandler", () => { expect(viewRoomPayload.room_id).toEqual(NATIVE_ROOM_BOB); expect(fakeCall).not.toBeNull(); - expect(fakeCall!.roomId).toEqual(VIRTUAL_ROOM_BOB); + expect(fakeCall!.roomId).toEqual(NATIVE_ROOM_BOB); expect(callHandler.roomIdForCall(fakeCall!)).toEqual(NATIVE_ROOM_BOB); }); @@ -346,8 +283,6 @@ describe("LegacyCallHandler", () => { it("should move calls between rooms when remote asserted identity changes", async () => { callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); - // We placed the call in Alice's room so it should start off there expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall); @@ -517,10 +452,7 @@ describe("LegacyCallHandler without third party protocols", () => { it("should still start a native call", async () => { callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice); - await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState); - // Check that a call was started: its room on the protocol level - // should be the virtual room expect(fakeCall).not.toBeNull(); expect(fakeCall!.roomId).toEqual(NATIVE_ROOM_ALICE); diff --git a/test/unit-tests/SlashCommands-test.tsx b/test/unit-tests/SlashCommands-test.tsx index 60bc145892..b30bc69176 100644 --- a/test/unit-tests/SlashCommands-test.tsx +++ b/test/unit-tests/SlashCommands-test.tsx @@ -14,7 +14,6 @@ import { type Command, Commands, getCommand } from "../../src/SlashCommands"; import { createTestClient } from "../test-utils"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom"; import SettingsStore from "../../src/settings/SettingsStore"; -import LegacyCallHandler from "../../src/LegacyCallHandler"; import { SdkContextClass } from "../../src/contexts/SDKContext"; import Modal from "../../src/Modal"; import WidgetUtils from "../../src/utils/WidgetUtils"; @@ -196,46 +195,6 @@ describe("SlashCommands", () => { }); }); - describe("/tovirtual", () => { - beforeEach(() => { - command = findCommand("tovirtual")!; - }); - - describe("isEnabled", () => { - describe("when virtual rooms are supported", () => { - beforeEach(() => { - jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true); - }); - - it("should return true for Room", () => { - setCurrentRoom(); - expect(command.isEnabled(client)).toBe(true); - }); - - it("should return false for LocalRoom", () => { - setCurrentLocalRoom(); - expect(command.isEnabled(client)).toBe(false); - }); - }); - - describe("when virtual rooms are not supported", () => { - beforeEach(() => { - jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false); - }); - - it("should return false for Room", () => { - setCurrentRoom(); - expect(command.isEnabled(client)).toBe(false); - }); - - it("should return false for LocalRoom", () => { - setCurrentLocalRoom(); - expect(command.isEnabled(client)).toBe(false); - }); - }); - }); - }); - describe("/part", () => { it("should part room matching alias if found", async () => { const room1 = new Room("room-id", client, client.getSafeUserId()); diff --git a/test/unit-tests/components/structures/RoomView-test.tsx b/test/unit-tests/components/structures/RoomView-test.tsx index 696df284fe..d4010700a7 100644 --- a/test/unit-tests/components/structures/RoomView-test.tsx +++ b/test/unit-tests/components/structures/RoomView-test.tsx @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import React, { createRef, type RefObject } from "react"; import { mocked, type MockedObject } from "jest-mock"; import { - ClientEvent, EventTimeline, EventType, type IEvent, @@ -68,7 +67,6 @@ import { DirectoryMember } from "../../../../src/utils/direct-messages"; import { createDmLocalRoom } from "../../../../src/utils/dm/createDmLocalRoom"; import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore"; import { SDKContext, SdkContextClass } from "../../../../src/contexts/SDKContext"; -import VoipUserMapper from "../../../../src/VoipUserMapper"; import WidgetUtils from "../../../../src/utils/WidgetUtils"; import { WidgetType } from "../../../../src/widgets/WidgetType"; import WidgetStore from "../../../../src/stores/WidgetStore"; @@ -119,7 +117,6 @@ describe("RoomView", () => { stores.client = cli; stores.rightPanelStore.useUnitTestClient(cli); - jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(undefined); crypto = cli.getCrypto()!; jest.spyOn(cli, "getCrypto").mockReturnValue(undefined); }); @@ -417,26 +414,6 @@ describe("RoomView", () => { await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument()); }); - describe("with virtual rooms", () => { - it("checks for a virtual room on initial load", async () => { - const { container } = await renderRoomView(); - expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId); - - // quick check that rendered without error - expect(container.querySelector(".mx_ErrorBoundary")).toBeFalsy(); - }); - - it("checks for a virtual room on room event", async () => { - await renderRoomView(); - expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledWith(room.roomId); - - act(() => cli.emit(ClientEvent.Room, room)); - - // called again after room event - expect(VoipUserMapper.sharedInstance().getVirtualRoomForRoom).toHaveBeenCalledTimes(2); - }); - }); - describe("video rooms", () => { beforeEach(async () => { await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet()); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 87c788d9f9..354036e282 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -35,7 +35,6 @@ import { forEachRight } from "lodash"; import TimelinePanel from "../../../../src/components/structures/TimelinePanel"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import { isCallEvent } from "../../../../src/components/structures/LegacyCallEventGrouper"; import { filterConsole, flushPromises, @@ -115,22 +114,6 @@ const setupTestData = (): [MatrixClient, Room, MatrixEvent[]] => { return [client, room, events]; }; -const setupOverlayTestData = (client: MatrixClient, mainEvents: MatrixEvent[]): [Room, MatrixEvent[]] => { - const virtualRoom = mkRoom(client, "virtualRoomId"); - const overlayEvents = mockEvents(virtualRoom, 5); - - // Set the event order that we'll be looking for in the timeline - overlayEvents[0].localTimestamp = 1000; - mainEvents[0].localTimestamp = 2000; - overlayEvents[1].localTimestamp = 3000; - overlayEvents[2].localTimestamp = 4000; - overlayEvents[3].localTimestamp = 5000; - mainEvents[1].localTimestamp = 6000; - overlayEvents[4].localTimestamp = 7000; - - return [virtualRoom, overlayEvents]; -}; - const expectEvents = (container: HTMLElement, events: MatrixEvent[]): void => { const eventTiles = container.querySelectorAll(".mx_EventTile"); const eventTileIds = [...eventTiles].map((tileElement) => tileElement.getAttribute("data-event-id")); @@ -518,298 +501,6 @@ describe("TimelinePanel", () => { expect(paginateSpy).toHaveBeenCalledWith(EventTimeline.FORWARDS, 1, false); }); - - it("advances the overlay timeline window", async () => { - const [client, room, events] = setupTestData(); - - const virtualRoom = mkRoom(client, "virtualRoomId"); - const virtualEvents = mockEvents(virtualRoom); - const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents); - - const props = { - ...getProps(room, events), - overlayTimelineSet, - }; - - const paginateSpy = jest.spyOn(TimelineWindow.prototype, "paginate").mockClear(); - - render(); - - await flushPromises(); - - const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 }); - const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true }; - client.emit(RoomEvent.Timeline, event, room, false, false, data); - - await flushPromises(); - - expect(paginateSpy).toHaveBeenCalledTimes(2); - }); - }); - - describe("with overlayTimeline", () => { - it("renders merged timeline", async () => { - const [client, room, events] = setupTestData(); - const virtualRoom = mkRoom(client, "virtualRoomId"); - const virtualCallInvite = new MatrixEvent({ - type: "m.call.invite", - room_id: virtualRoom.roomId, - event_id: `virtualCallEvent1`, - origin_server_ts: 0, - }); - virtualCallInvite.localTimestamp = 2; - const virtualCallMetaEvent = new MatrixEvent({ - type: "org.matrix.call.sdp_stream_metadata_changed", - room_id: virtualRoom.roomId, - event_id: `virtualCallEvent2`, - origin_server_ts: 0, - }); - virtualCallMetaEvent.localTimestamp = 2; - const virtualEvents = [virtualCallInvite, ...mockEvents(virtualRoom), virtualCallMetaEvent]; - const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents); - - const { container } = render( - , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), - ); - await waitFor(() => - expectEvents(container, [ - // main timeline events are included - events[0], - events[1], - // virtual timeline call event is included - virtualCallInvite, - // virtual call event has no tile renderer => not rendered - ]), - ); - }); - - it.each([ - ["when it starts with no overlay events", true], - ["to get enough overlay events", false], - ])("expands the initial window %s", async (_s, startWithEmptyOverlayWindow) => { - const [client, room, events] = setupTestData(); - const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events); - - let overlayEventsPage1: MatrixEvent[]; - let overlayEventsPage2: MatrixEvent[]; - let overlayEventsPage3: MatrixEvent[]; - if (startWithEmptyOverlayWindow) { - overlayEventsPage1 = overlayEvents.slice(0, 3); - overlayEventsPage2 = []; - overlayEventsPage3 = overlayEvents.slice(3, 5); - } else { - overlayEventsPage1 = overlayEvents.slice(0, 2); - overlayEventsPage2 = overlayEvents.slice(2, 3); - overlayEventsPage3 = overlayEvents.slice(3, 5); - } - - // Start with only page 2 of the overlay events in the window - const [overlayTimeline, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEventsPage2); - setupPagination(client, overlayTimeline, overlayEventsPage1, overlayEventsPage3); - - const { container } = render( - , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), - ); - - await waitFor(() => - expectEvents(container, [ - overlayEvents[0], - events[0], - overlayEvents[1], - overlayEvents[2], - overlayEvents[3], - events[1], - overlayEvents[4], - ]), - ); - }); - - it("extends overlay window beyond main window at the start of the timeline", async () => { - const [client, room, events] = setupTestData(); - const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events); - // Delete event 0 so the TimelinePanel will still leave some stuff - // unloaded for us to test with - events.shift(); - - const overlayEventsPage1 = overlayEvents.slice(0, 2); - const overlayEventsPage2 = overlayEvents.slice(2, 5); - - // Start with only page 2 of the overlay events in the window - const [overlayTimeline, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEventsPage2); - setupPagination(client, overlayTimeline, overlayEventsPage1, null); - - const { container } = render( - , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), - ); - - await waitFor(() => - expectEvents(container, [ - // These first two are the newly loaded events - overlayEvents[0], - overlayEvents[1], - overlayEvents[2], - overlayEvents[3], - events[0], - overlayEvents[4], - ]), - ); - }); - - it("extends overlay window beyond main window at the end of the timeline", async () => { - const [client, room, events] = setupTestData(); - const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events); - // Delete event 1 so the TimelinePanel will still leave some stuff - // unloaded for us to test with - events.pop(); - - const overlayEventsPage1 = overlayEvents.slice(0, 2); - const overlayEventsPage2 = overlayEvents.slice(2, 5); - - // Start with only page 1 of the overlay events in the window - const [overlayTimeline, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEventsPage1); - setupPagination(client, overlayTimeline, null, overlayEventsPage2); - - const { container } = render( - , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), - ); - - await waitFor(() => - expectEvents(container, [ - overlayEvents[0], - events[0], - overlayEvents[1], - // These are the newly loaded events - overlayEvents[2], - overlayEvents[3], - overlayEvents[4], - ]), - ); - }); - - it("paginates", async () => { - const [client, room, events] = setupTestData(); - const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events); - - const eventsPage1 = events.slice(0, 1); - const eventsPage2 = events.slice(1, 2); - - // Start with only page 1 of the main events in the window - const [timeline, timelineSet] = mkTimeline(room, eventsPage1); - const [, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEvents); - setupPagination(client, timeline, null, eventsPage2); - - await withScrollPanelMountSpy(async (mountSpy) => { - const { container } = render( - , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), - ); - - await waitFor(() => expectEvents(container, [overlayEvents[0], events[0]])); - - // ScrollPanel has no chance of working in jsdom, so we've no choice - // but to do some shady stuff to trigger the fill callback by hand - const scrollPanel = mountSpy.mock.contexts[0] as ScrollPanel; - scrollPanel.props.onFillRequest!(false); - - await waitFor(() => - expectEvents(container, [ - overlayEvents[0], - events[0], - overlayEvents[1], - overlayEvents[2], - overlayEvents[3], - events[1], - overlayEvents[4], - ]), - ); - }); - }); - - it.each([ - ["down", "main", true, false], - ["down", "overlay", true, true], - ["up", "main", false, false], - ["up", "overlay", false, true], - ])("unpaginates %s to an event from the %s timeline", async (_s1, _s2, backwards, fromOverlay) => { - const [client, room, events] = setupTestData(); - const [virtualRoom, overlayEvents] = setupOverlayTestData(client, events); - - let marker: MatrixEvent; - let expectedEvents: MatrixEvent[]; - if (backwards) { - if (fromOverlay) { - marker = overlayEvents[1]; - // Overlay events 0−1 and event 0 should be unpaginated - // Overlay events 2−3 should be hidden since they're at the edge of the window - expectedEvents = [events[1], overlayEvents[4]]; - } else { - marker = events[0]; - // Overlay event 0 and event 0 should be unpaginated - // Overlay events 1−3 should be hidden since they're at the edge of the window - expectedEvents = [events[1], overlayEvents[4]]; - } - } else { - if (fromOverlay) { - marker = overlayEvents[4]; - // Only the last overlay event should be unpaginated - expectedEvents = [ - overlayEvents[0], - events[0], - overlayEvents[1], - overlayEvents[2], - overlayEvents[3], - events[1], - ]; - } else { - // Get rid of overlay event 4 so we can test the case where no overlay events get unpaginated - overlayEvents.pop(); - marker = events[1]; - // Only event 1 should be unpaginated - // Overlay events 1−2 should be hidden since they're at the edge of the window - expectedEvents = [overlayEvents[0], events[0]]; - } - } - - const [, overlayTimelineSet] = mkTimeline(virtualRoom, overlayEvents); - - await withScrollPanelMountSpy(async (mountSpy) => { - const { container } = render( - , - withClientContextRenderOptions(MatrixClientPeg.safeGet()), - ); - - await waitFor(() => - expectEvents(container, [ - overlayEvents[0], - events[0], - overlayEvents[1], - overlayEvents[2], - overlayEvents[3], - events[1], - ...(!backwards && !fromOverlay ? [] : [overlayEvents[4]]), - ]), - ); - - // ScrollPanel has no chance of working in jsdom, so we've no choice - // but to do some shady stuff to trigger the unfill callback by hand - const scrollPanel = mountSpy.mock.contexts[0] as ScrollPanel; - scrollPanel.props.onUnfillRequest!(backwards, marker.getId()!); - - await waitFor(() => expectEvents(container, expectedEvents)); - }); - }); }); describe("when a thread updates", () => { diff --git a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 0e85152567..1e7ceea447 100644 --- a/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1952,7 +1952,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` aria-label="Open room settings" aria-live="off" class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52" - data-color="5" + data-color="3" data-testid="avatar-img" data-type="round" role="button" @@ -1979,7 +1979,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = ` - !18:example.org + !16:example.org diff --git a/test/unit-tests/stores/room-list/filters/VisibilityProvider-test.ts b/test/unit-tests/stores/room-list/filters/VisibilityProvider-test.ts index 3ba1fb3de6..95e0a3e103 100644 --- a/test/unit-tests/stores/room-list/filters/VisibilityProvider-test.ts +++ b/test/unit-tests/stores/room-list/filters/VisibilityProvider-test.ts @@ -10,22 +10,10 @@ import { mocked } from "jest-mock"; import { type Room, RoomType } from "matrix-js-sdk/src/matrix"; import { VisibilityProvider } from "../../../../../src/stores/room-list/filters/VisibilityProvider"; -import LegacyCallHandler from "../../../../../src/LegacyCallHandler"; -import VoipUserMapper from "../../../../../src/VoipUserMapper"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../../src/models/LocalRoom"; import { RoomListCustomisations } from "../../../../../src/customisations/RoomList"; import { createTestClient } from "../../../../test-utils"; -jest.mock("../../../../../src/VoipUserMapper", () => ({ - sharedInstance: jest.fn(), -})); - -jest.mock("../../../../../src/LegacyCallHandler", () => ({ - instance: { - getSupportsVirtualRooms: jest.fn(), - }, -})); - jest.mock("../../../../../src/customisations/RoomList", () => ({ RoomListCustomisations: { isRoomVisible: jest.fn(), @@ -46,16 +34,6 @@ const createLocalRoom = (): LocalRoom => { }; describe("VisibilityProvider", () => { - let mockVoipUserMapper: VoipUserMapper; - - beforeEach(() => { - mockVoipUserMapper = { - onNewInvitedRoom: jest.fn(), - isVirtualRoom: jest.fn(), - } as unknown as VoipUserMapper; - mocked(VoipUserMapper.sharedInstance).mockReturnValue(mockVoipUserMapper); - }); - describe("instance", () => { it("should return an instance", () => { const visibilityProvider = VisibilityProvider.instance; @@ -64,28 +42,7 @@ describe("VisibilityProvider", () => { }); }); - describe("onNewInvitedRoom", () => { - it("should call onNewInvitedRoom on VoipUserMapper.sharedInstance", async () => { - const room = {} as unknown as Room; - await VisibilityProvider.instance.onNewInvitedRoom(room); - expect(mockVoipUserMapper.onNewInvitedRoom).toHaveBeenCalledWith(room); - }); - }); - describe("isRoomVisible", () => { - describe("for a virtual room", () => { - beforeEach(() => { - mocked(LegacyCallHandler.instance.getSupportsVirtualRooms).mockReturnValue(true); - mocked(mockVoipUserMapper.isVirtualRoom).mockReturnValue(true); - }); - - it("should return return false", () => { - const room = createRoom(); - expect(VisibilityProvider.instance.isRoomVisible(room)).toBe(false); - expect(mockVoipUserMapper.isVirtualRoom).toHaveBeenCalledWith(room); - }); - }); - it("should return false without room", () => { expect(VisibilityProvider.instance.isRoomVisible()).toBe(false); });