From aa821a5b6fbe291fafaf232849dd23e57a39a1e7 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 16 Apr 2025 09:36:34 +0100 Subject: [PATCH 1/8] 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); }); From 427e61309bbb6366c568f5bf856373e672e9f06f Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 16 Apr 2025 09:38:00 +0100 Subject: [PATCH 2/8] Update team members in triage-assigned.yml (#29751) --- .github/workflows/triage-assigned.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/triage-assigned.yml b/.github/workflows/triage-assigned.yml index e43eb94618..f190122a1c 100644 --- a/.github/workflows/triage-assigned.yml +++ b/.github/workflows/triage-assigned.yml @@ -11,7 +11,8 @@ jobs: runs-on: ubuntu-24.04 if: | contains(github.event.issue.assignees.*.login, 't3chguy') || - contains(github.event.issue.assignees.*.login, 'andybalaam') || + contains(github.event.issue.assignees.*.login, 'florianduros') || + contains(github.event.issue.assignees.*.login, 'dbkr') || contains(github.event.issue.assignees.*.login, 'MidhunSureshR') steps: - uses: actions/add-to-project@main From b511bf064db5e7d9f85bc69429f7989881b4cdc2 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 16 Apr 2025 11:23:23 +0200 Subject: [PATCH 3/8] New room list: new visual for invitation (#29773) * feat: rework invitation styling in room list item * test: update notification decoration test * test: add test for vm * test(e2e): update to new invitation styling --- .../room-list-item-invited-linux.png | Bin 2945 -> 2841 bytes .../RoomListPanel/_RoomListItemView.pcss | 4 --- .../roomlist/RoomListItemViewModel.tsx | 10 ++++++-- .../views/rooms/NotificationDecoration.tsx | 3 ++- .../roomlist/RoomListItemViewModel-test.tsx | 23 +++++++++++++++++- .../NotificationDecoration-test.tsx.snap | 14 ++++++++--- 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png index aa35ea711815c11e556bf749fff65826a18cee5c..4d9fd3ccd2dbc44b101ab0af211c20edae160e36 100644 GIT binary patch delta 2821 zcmV+g3;Ohd7nv52Fnc~_(A2oOJdj|Qvg9wU{CK`}T z4Xmb^dxkqZ2hgy3P;>xzC83(GcO2{!<{d68JyRnZ7*cW=fvnG5={`T6X<$Mab&>#p zaiu|B7#2%PPJdSC9x5B`7pmFCz?iV$BbNU3<2QH+9u{$&hsPp#T75N=02L zOsQm3_IduTP8a0KB%8t!uixw^%eFR^A^)kvG>fB0mYlV9+aHmgXmwNhQiA zGKGBc<$uJI6$jr^C%O9ihF!aMsU_9X6aY;4+>_(gbFD2k$* zMnDjxySryoY#f`-K7K6O+uKu%2mp+OuWww-tg9ZDs6J1}VKK{QFy8fKDLY0{gMcfl zOuwaVlFIy?aXWH%Akj$m&z5xhv>iLP>EUn=4uAGq3}kL*79SVe-rm9V^%)o#*t&gZ zNlEFyk`4w1&Cx1x0A5mcoe$p6I-~yaMVuz4-4rJ?ilPG))`jNsdt)?5I4qVlm*1PY zV5-U^4Gj$!MTToV5Dtg)V=+e_^Z9(O;{foIYwG$<(=NY0>QGV_6wWU+6mc{iNquSt zZ+~fF`@If-7dxp-;w-ImpB-*a*Gyp)w46}|e0A98p+-kNpmmo8t~-QCmM+j}e}H6%2G z=1BJsn45g`IG4*q5JV=E)z#IbX5( z7l+fDeDpYD7IP}yb=&rxPo8w?`sD|IH7J522x4YHaww~naO0semZ`B%%!D;9=^@cI1f*Q+QLOJASaINTr4 z&cMK6@uG;T>Kk2M-HRf^gM#M#@@rkmwNhW-*~v!^o12^U^z_7U-j7oX1!3KR;3dlL_& z{pak}!XFn!M*MIk=ebSGmn|JBTM65@E?FGe(!yT9VH20fOFe#crt6H{yn>ImY;SCA z5(tEPdV1Qv*x+^XX;C&2hhOf>2zH#lwcGQ#8WVzC(R@d1<@IVtGq?~Tg*>ge^NLkpID78t(T!zbqQw^$4& zIW*eV#PsKOc6?4o)q_@b0&2Pc!i)0kNzv2C78d4ArcdfuCx0s{Djz-S93GYg`1_Ga zhCH6a>LZm(rBZ3|+@KXJmf`jABY(u=uy(e#wY9Z!2P^qlDnCh6(h)xDK!>EV|NTTz z+0K3=^`CFMcblCvrvL6ElJyO*3x`M#m%*)Gc8fePmrj4Vhx%6ZI##L6AFlnp@axl{r{iTH4r5ZoA*!*w`eK$%H~-RaLb}B$_tO$;QS; zQ4^6!)Zyy@URxMc{@tEgcf;{e!q&XZ_?&ddLkV(4L4W1HG=<+6j4VjK5Bf)!twK64 zI#ZpdxVp|LF1{v{$=0o1ZDnO8cK}XL&(gx2$K#zolh)VQ$6_&=zCIT(WxxCG1_r~s z_1?YW;%oDQgJ#cS`1;Pax2G0d{ZTwLWM^x8<3^1@AT%^IP@aIP>BQ(2Q9E}2efMYo zD6gpO>3``hD7fn3?iL#ImW746q9!aBwxs|%@a*@6r-r4{FU#{4by5C(L1A5(Je|^Z z^P%P$s%7$Yg+H8X$Jp4|-;Y&VT1Fy~{ry-3efdRG1bzL;i14cFnzFJAK94_Z)*GLH z_KCAIJvXo5^uN=vSZvtBkhl+HO-xLTjg1c<+<$-ao6|)<71!0(uU)gMw6t8&{K8Wh z3}#+%5P~3yi3d)dK7*p@+@QdZcO}@^OhynyQB!|ESGTVN06L*?|ApJRcT!hns_w7h zI_MFx8Bxx2Xj&x<0FakiSG_bNRkJS3HZF1TQq-z&e!!(QBGN!{N()TG2ih}^SUVjfSNSAZff}q#yX|fO)0Aj+{gT# z9!G{n5^Ym`UQYVhy|?1X=7u_q2>{S7W&1A}S&~?*bbMv;ob_)&Q2^j2r((aRg$e%F zbPHV#M)j}`+3KgOXaImQqM|Mk$oetqQGX^@~cNgE4IQCfY*@f{h%jI zGKxu$B9QQ-8OCrTF*aib$yCMaAOOJ2K;8Sw4D(N0087cT>t>cgSr3!koQtw#njIme9Nkl`;M(6v3@1 zg0?E|Qny#BfL5hid%9Vxwdzxm_ry|ZEn2HcE1OoquslIf``Ba&APOj(kcBLejL0tdH+{j71RX)fGVg90031ae8c*s@`{|g(zMFf>ULpgudu5ZK@pmX9&Nmy zg+1MKp|f+KEl~%G4gg~kI@J3ed%Go>hx52+N~EGbHQlHN6xK@jxiKs~W9qPz1OSYx zA=HI_nLIbKIDg~oyxty(mP7Q6sPne5Igwr@GI1D50suypwsk=wke*z1zB<1V$1z;y zIIN9dNHw6~7z+RxeOlFpCv~0iE6zRPb>cpZx$XGP$x9e^rnn;j07i}Gr9ok5?}_Cp z!zl{CSC*CM1RcG@2?78Z9UA5f@;=3(1>e?}JQ|UM*?)LXioLuVzNY}-uOn9|GHc3; z9@gJ!X}r_&07X&v36tF?O!j`wdanI6B0;N9pN6`Sbu|C-XE#PJXJp}?z&Ts}M=&4& z;7|2k?JF;wDr~A7wAI(r_Pe*%Tbs}|#!*)n1RcG5JpU;b$%dEe|Dn>zpB6_M(Dk+H zEEbEod4KsN63Nfcmr5D(vZY)uFD<>>(%QlaU>h47vpfiyVzb0Y#C(jXuYlcgMHxnYWEiAfoE~ON6qY_vfnb^QZFiiru?E z`f%^ZygHtiL!Q;v-o9`DfgL+{=j3v6Z%ftD(SNyQ*-8(u8O6mV_#yx>3KEKQ)~9}} zz9=9FB9uuZFC`@u=cw<7Sr^27X<^D;yh*C_pL6%bd=1$TaB^~ti`%hl$95YVYa9on zo0`T%M}4>_j^*zs5{Wjy`<|n->$PjwafAZkWyb4j+n#Y=`~KTA&hxt3)O5hC3)T4z z@_#OH4#1e8P%2kl zNEG#{PVF7(7HKP@YCa$>()t3P)9 zq|Xf26epJ*J3nY^Yai)PJhqHN=`mIapD9|FOQbymXeZEg+d{f$_fi_@%emC z0NZr@@6!Y-l^PxvYHiIVkw_sS^A?4LqA2p!p(9`HKPXoyLPO{4>gpaic<9jAM?@k~ zbxlo7?2gpbG&@^ccXzi-scHA_{f3p#wQJXF>v)S7g*!OdUAlC6`}=W~Rex1MGXrg` ztuOp^F*;^@T^&y@mtVY?I%v~Ckk-eK6Hh0Z(oLCGlYdA#ziIQ9E9n_N-d-jqFF(A z6joGxyS=?*QP{%Z;F-Vuw=DN2*Wcgw*wMpux@l);XH4vljEt;#b7z;8SMciVXU_^c z@Z~;ZW8;E?!lD!lGMT%( zo2`v?S$Rc6V`Eu)d4C;`=kD%iYio-YM5$EP*Ee)@bmr$5x_S7#W@+Q%=6xe4S11y- zcXT#4HzNq*(-L69d;o-{Q*WLjA{J2{~!N~KbLe7vbt zYE@NrQ&SUGlUOW95JXp3m%%V65C{}1g-WFw8Wn7`uxvkW^@+IFX3N_1qFqzt&jWr<$;_48ckmyz^pzBg+ifF%$^mz zeECwc4te00L=w?z^5oLeQkBkXK9J;iC>liliUsWu}^enDYfgGa9~wX=7xA zlgVT@n>B8nF@HC&p!CjNCezB(%Y*RT{hkA#VWm=uqNs%h!@z*X=kpyMr}+E(y1Tn} zcXvO1+D)ZWCr+G*AV^VBu|Oa|5aj;->e{-&?;d-Wxw*Ne<)p?3O_h~ZN~KaFkrWjb zOQljrM+ZwwOROecU0scl0Z`A?!g9n1V$K%~&1hYZdVla8PoohpI#V3%U0kN;^sB9 z-{05Tnwfp|S6N@5)#S;yZKs#A?E<3zMcY8t*jYIG8pjO=XO1KqDg~P5_(B z&7;u_I05YEZ}0)K%pXLc}xAp7?p{3huPilVcEgZAv+ zWobDHK@hB_o}R(KLjpj1YEJugU2R_RpWqmX`FzB^jQa_zf7F;0iXv}p@Cn}=L?ED7 zzT##b$y3NRUpYsei*TNaxtIq4(4I&possZa*^{B2#huM26#rwd-XDu=Dlpd<9Q>Jo z(SPP^L>+Po|uGdn0)$*Q8`3@%+-wSVcj@^OR?|rROE9F3t}hYn8|H zLvN0FSO8vSM+&aSWSrBo^Wk|*qy1*7>7ed;%K1A2NR$ztGLR_51-m#S8xsH!?KdkS zbQO)PVLJk2s_vP^k?MRP18Uw6`e_UQ%Q>gJTLr_+mChWuGc%GlUc-$50Du=x^M59g z<0cqxNr|A+Fqba1p>Luab#6Hn1pvkbZQm;+>Jpz=ky3vf&&JSG*v$2(7VDe9J-+~q z4MTVjol>DJ{Ndi^eK!Q{A}xm)nCgYb1^TUULkSx8eFOl!Jcj( room.tags); const isArchived = Boolean(roomTags[DefaultTagID.Archived]); - const showHoverMenu = - hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived); const notificationState = useMemo(() => RoomNotificationStateStore.instance.getRoomState(room), [room]); + const invited = notificationState.invited; const a11yLabel = getA11yLabel(room, notificationState); const isBold = notificationState.hasAnyNotificationOrActivity; + // We don't want to show the hover menu if + // - there is an invitation for this room + // - the user doesn't have access to both notification and more options menus + const showHoverMenu = + !invited && + (hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived)); + // Video room const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom(); // EC video call or video room diff --git a/src/components/views/rooms/NotificationDecoration.tsx b/src/components/views/rooms/NotificationDecoration.tsx index cfb82c461c..9cc1bee738 100644 --- a/src/components/views/rooms/NotificationDecoration.tsx +++ b/src/components/views/rooms/NotificationDecoration.tsx @@ -10,6 +10,7 @@ import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/ment import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error-solid"; import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-off-solid"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; +import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid"; import { UnreadCounter, Unread } from "@vector-im/compound-web"; import { Flex } from "../../utils/Flex"; @@ -56,7 +57,7 @@ export function NotificationDecoration({ > {isUnsetMessage && } {hasVideoCall && } - {invited && } + {invited && } {isMention && } {(isMention || isNotification) && } {isActivityNotification && } diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx index 867a909f2e..be309b36ed 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListItemViewModel-test.tsx @@ -33,6 +33,10 @@ describe("RoomListItemViewModel", () => { room = mkStubRoom("roomId", "roomName", matrixClient); }); + afterEach(() => { + jest.resetAllMocks(); + }); + it("should dispatch view room action on openRoom", async () => { const { result: vm } = renderHook( () => useRoomListItemViewModel(room), @@ -68,6 +72,20 @@ describe("RoomListItemViewModel", () => { expect(vm.current.showHoverMenu).toBe(true); }); + it("should not show hover menu if user has an invitation notification", async () => { + mocked(hasAccessToOptionsMenu).mockReturnValue(true); + + const notificationState = new RoomNotificationState(room, false); + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); + jest.spyOn(notificationState, "invited", "get").mockReturnValue(false); + + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); + expect(vm.current.showHoverMenu).toBe(true); + }); + describe("a11yLabel", () => { let notificationState: RoomNotificationState; beforeEach(() => { @@ -108,7 +126,10 @@ describe("RoomListItemViewModel", () => { }, ])("should return the $label label", ({ mock, expected }) => { mock?.(); - const { result: vm } = renderHook(() => useRoomListItemViewModel(room)); + const { result: vm } = renderHook( + () => useRoomListItemViewModel(room), + withClientContextRenderOptions(room.client), + ); expect(vm.current.a11yLabel).toBe(expected); }); }); diff --git a/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap b/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap index be81664eb8..33ee60102b 100644 --- a/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/__snapshots__/NotificationDecoration-test.tsx.snap @@ -23,11 +23,17 @@ exports[` should render the invitation decoration 1`] data-testid="notification-decoration" style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1-5x); --mx-flex-wrap: nowrap;" > - - 1 - + + `; From db9428de874fe14bc8f141ec63d8b24aee503c36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:34:01 +0200 Subject: [PATCH 4/8] Update react monorepo (#29765) * Update react monorepo * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 10 +-- .../__snapshots__/FilePanel-test.tsx.snap | 2 +- .../__snapshots__/RoomView-test.tsx.snap | 72 +++++++++--------- .../SpaceHierarchy-test.tsx.snap | 32 ++++---- .../__snapshots__/ThreadPanel-test.tsx.snap | 4 +- .../DecoratedRoomAvatar-test.tsx.snap | 4 +- .../WithPresenceIndicator-test.tsx.snap | 6 +- .../BeaconListItem-test.tsx.snap | 2 +- .../__snapshots__/DialogSidebar-test.tsx.snap | 2 +- .../ShareLatestLocation-test.tsx.snap | 2 +- .../ConfirmRejectInviteDialog-test.tsx.snap | 16 ++-- .../DevtoolsDialog-test.tsx.snap | 4 +- ...nageRestrictedJoinRuleDialog-test.tsx.snap | 4 +- .../ReportRoomDialog-test.tsx.snap | 6 +- .../ServerPickerDialog-test.tsx.snap | 2 +- .../__snapshots__/AppTile-test.tsx.snap | 6 +- .../__snapshots__/FacePile-test.tsx.snap | 2 +- .../__snapshots__/InfoTooltip-test.tsx.snap | 2 +- .../LabelledCheckbox-test.tsx.snap | 4 +- .../__snapshots__/RoomFacePile-test.tsx.snap | 4 +- .../__snapshots__/SettingsField-test.tsx.snap | 4 +- .../LocationShareMenu-test.tsx.snap | 4 +- .../__snapshots__/MLocationBody-test.tsx.snap | 4 +- .../__snapshots__/PollHistory-test.tsx.snap | 4 +- .../__snapshots__/PollListItem-test.tsx.snap | 2 +- .../PollListItemEnded-test.tsx.snap | 2 +- .../__snapshots__/BaseCard-test.tsx.snap | 2 +- .../ExtensionsCard-test.tsx.snap | 4 +- .../PinnedMessagesCard-test.tsx.snap | 22 +++--- .../RoomSummaryCard-test.tsx.snap | 12 +-- .../__snapshots__/UserInfo-test.tsx.snap | 4 +- .../__snapshots__/RoomHeader-test.tsx.snap | 8 +- .../VideoRoomChatButton-test.tsx.snap | 2 +- .../RoomListHeaderView-test.tsx.snap | 12 +-- .../RoomListItemMenuView-test.tsx.snap | 16 ++-- .../__snapshots__/RoomListPanel-test.tsx.snap | 2 +- .../PinnedEventTile-test.tsx.snap | 16 ++-- .../ReadReceiptGroup-test.tsx.snap | 8 +- .../ThirdPartyMemberInfo-test.tsx.snap | 4 +- .../MemberTileView-test.tsx.snap | 4 +- .../LayoutSwitcher-test.tsx.snap | 20 ++--- .../__snapshots__/Notifications-test.tsx.snap | 8 +- .../__snapshots__/SetIdServer-test.tsx.snap | 4 +- .../ThemeChoicePanel-test.tsx.snap | 76 +++++++++---------- .../FilteredDeviceListHeader-test.tsx.snap | 4 +- .../__snapshots__/AdvancedPanel-test.tsx.snap | 8 +- .../ChangeRecoveryKey-test.tsx.snap | 12 +-- .../Notifications2-test.tsx.snap | 40 +++++----- .../AppearanceUserSettingsTab-test.tsx.snap | 32 ++++---- .../SecurityUserSettingsTab-test.tsx.snap | 4 +- .../SessionManagerTab-test.tsx.snap | 2 +- .../SidebarUserSettingsTab-test.tsx.snap | 48 ++++++------ .../__snapshots__/SpacePanel-test.tsx.snap | 8 +- .../SpaceSettingsVisibilityTab-test.tsx.snap | 6 +- .../ThreadsActivityCentre-test.tsx.snap | 64 ++++++++-------- test/unit-tests/hooks/useProfileInfo-test.tsx | 7 +- .../hooks/usePublicRoomDirectory-test.tsx | 3 +- .../hooks/useUserDirectory-test.tsx | 6 +- .../__snapshots__/link-tooltip-test.tsx.snap | 2 +- yarn.lock | 46 +++++------ 60 files changed, 362 insertions(+), 360 deletions(-) diff --git a/package.json b/package.json index 159ca2879b..4773ee3da9 100644 --- a/package.json +++ b/package.json @@ -68,10 +68,10 @@ "postinstall": "patch-package" }, "resolutions": { - "**/pretty-format/react-is": "19.0.0", + "**/pretty-format/react-is": "19.1.0", "@playwright/test": "1.51.1", - "@types/react": "19.0.10", - "@types/react-dom": "19.0.4", + "@types/react": "19.1.1", + "@types/react-dom": "19.1.2", "oidc-client-ts": "3.2.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001713", @@ -211,9 +211,9 @@ "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "19.0.10", + "@types/react": "19.1.1", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "19.0.4", + "@types/react-dom": "19.1.2", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.15.0", "@types/semver": "^7.5.8", diff --git a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap index cc900cdf5a..f311521960 100644 --- a/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap +++ b/test/unit-tests/components/structures/__snapshots__/FilePanel-test.tsx.snap @@ -19,7 +19,7 @@ exports[`FilePanel renders empty state 1`] = `