You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-08-09 14:42:51 +03:00
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
This commit is contained in:
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@@ -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;
|
||||
|
@@ -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<LegacyCallHandl
|
||||
private transferees = new Map<string, MatrixCall>(); // 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<LegacyCallHandl
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
|
||||
* if a voip_mxid_translate_pattern is set in the config)
|
||||
* Gets the user-facing room associated with a call
|
||||
*/
|
||||
public roomIdForCall(call?: MatrixCall): string | null {
|
||||
if (!call) return null;
|
||||
@@ -195,7 +171,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
}
|
||||
}
|
||||
|
||||
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) ?? call.roomId ?? null;
|
||||
return call.roomId ?? null;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
@@ -278,12 +254,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
this.supportsPstnProtocol = null;
|
||||
}
|
||||
|
||||
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
|
||||
this.supportsSipNativeVirtual = Boolean(
|
||||
protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL],
|
||||
);
|
||||
}
|
||||
|
||||
this.emit(LegacyCallHandlerEvent.ProtocolSupport);
|
||||
} catch (e) {
|
||||
if (maxTries === 1) {
|
||||
@@ -305,10 +275,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
return this.supportsPstnProtocol ?? false;
|
||||
}
|
||||
|
||||
public getSupportsVirtualRooms(): boolean | null {
|
||||
return this.supportsSipNativeVirtual;
|
||||
}
|
||||
|
||||
public async pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
|
||||
try {
|
||||
return await MatrixClientPeg.safeGet().getThirdpartyUser(
|
||||
@@ -323,28 +289,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
}
|
||||
}
|
||||
|
||||
public async sipVirtualLookup(nativeMxid: string): Promise<ThirdpartyLookupResponse[]> {
|
||||
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<ThirdpartyLookupResponse[]> {
|
||||
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<LegacyCallHandl
|
||||
}
|
||||
|
||||
const newAssertedIdentity = call.getRemoteAssertedIdentity()?.id;
|
||||
let newNativeAssertedIdentity = newAssertedIdentity;
|
||||
if (newAssertedIdentity) {
|
||||
const response = await this.sipNativeLookup(newAssertedIdentity);
|
||||
if (response.length && response[0].fields.lookup_success) {
|
||||
newNativeAssertedIdentity = response[0].userid;
|
||||
}
|
||||
}
|
||||
logger.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`);
|
||||
|
||||
if (newNativeAssertedIdentity) {
|
||||
this.assertedIdentityNativeUsers.set(call.callId, newNativeAssertedIdentity);
|
||||
if (newAssertedIdentity) {
|
||||
this.assertedIdentityNativeUsers.set(call.callId, newAssertedIdentity);
|
||||
|
||||
// If we don't already have a room with this user, make one. This will be slightly odd
|
||||
// if they called us because we'll be inviting them, but there's not much we can do about
|
||||
// this if we want the actual, native room to exist (which we do). This is why it's
|
||||
// important to only obey asserted identity in trusted environments, since anyone you're
|
||||
// on a call with can cause you to send a room invite to someone.
|
||||
await ensureDMExists(MatrixClientPeg.safeGet(), newNativeAssertedIdentity);
|
||||
await ensureDMExists(MatrixClientPeg.safeGet(), newAssertedIdentity);
|
||||
|
||||
const newMappedRoomId = this.roomIdForCall(call);
|
||||
logger.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`);
|
||||
@@ -810,24 +746,10 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
|
||||
private async placeMatrixCall(roomId: string, type: CallType, transferee?: MatrixCall): Promise<void> {
|
||||
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<LegacyCallHandl
|
||||
}
|
||||
const userId = results[0].userid;
|
||||
|
||||
// Now check to see if this is a virtual user, in which case we should find the
|
||||
// native user
|
||||
let nativeUserId;
|
||||
if (this.getSupportsVirtualRooms()) {
|
||||
const nativeLookupResults = await this.sipNativeLookup(userId);
|
||||
const lookupSuccess = nativeLookupResults.length > 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");
|
||||
}
|
||||
|
@@ -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<keyof EmittedEvents, EmittedEvents
|
||||
|
||||
// XXX: exported for tests
|
||||
public evaluateEvent(ev: MatrixEvent): void {
|
||||
let roomId = ev.getRoomId()!;
|
||||
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
||||
// Attempt to translate a virtual room to a native one
|
||||
const nativeRoomId = VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(roomId);
|
||||
if (nativeRoomId) {
|
||||
roomId = nativeRoomId;
|
||||
}
|
||||
}
|
||||
const roomId = ev.getRoomId()!;
|
||||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) {
|
||||
// e.g we are in the process of joining a room.
|
||||
|
@@ -52,7 +52,6 @@ import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpD
|
||||
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
||||
import { TimelineRenderingType } from "./contexts/RoomContext";
|
||||
import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||
import VoipUserMapper from "./VoipUserMapper";
|
||||
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
|
||||
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
@@ -743,28 +742,6 @@ export const Commands = [
|
||||
},
|
||||
category: CommandCategories.advanced,
|
||||
}),
|
||||
new Command({
|
||||
command: "tovirtual",
|
||||
description: _td("slash_command|tovirtual"),
|
||||
category: CommandCategories.advanced,
|
||||
isEnabled(cli): boolean {
|
||||
return !!LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom(cli);
|
||||
},
|
||||
runFn: (cli, roomId) => {
|
||||
return success(
|
||||
(async (): Promise<void> => {
|
||||
const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId);
|
||||
if (!room) throw new UserFriendlyError("slash_command|tovirtual_not_found");
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: "SlashCommand",
|
||||
metricsViaKeyboard: true,
|
||||
});
|
||||
})(),
|
||||
);
|
||||
},
|
||||
}),
|
||||
new Command({
|
||||
command: "query",
|
||||
description: _td("slash_command|query"),
|
||||
|
@@ -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<string, string>();
|
||||
|
||||
public static sharedInstance(): VoipUserMapper {
|
||||
if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
|
||||
return window.mxVoipUserMapper;
|
||||
}
|
||||
|
||||
private async userToVirtualUser(userId: string): Promise<string | null> {
|
||||
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<string | null> {
|
||||
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<string | null> {
|
||||
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<Room | undefined> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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<IRoomProps, IRoomState> {
|
||||
return this.messagePanel.canResetTimeline();
|
||||
};
|
||||
|
||||
private loadVirtualRoom = async (room?: Room): Promise<void> => {
|
||||
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<IRoomProps, IRoomState> {
|
||||
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<IRoomProps, IRoomState> {
|
||||
<TimelinePanel
|
||||
ref={this.gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
|
||||
overlayTimelineSetFilter={isCallEvent}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
|
||||
|
@@ -30,7 +30,7 @@ import {
|
||||
ThreadEvent,
|
||||
ReceiptType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { debounce, findLastIndex } from "lodash";
|
||||
import { debounce } from "lodash";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
@@ -73,25 +73,12 @@ const debuglog = (...args: any[]): void => {
|
||||
}
|
||||
};
|
||||
|
||||
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<IProps, IState> {
|
||||
private readonly messagePanel = createRef<MessagePanel>();
|
||||
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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
// 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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
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,13 +695,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
// timeline window.
|
||||
//
|
||||
// see https://github.com/vector-im/vector-web/issues/1035
|
||||
this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false)
|
||||
.then(() => {
|
||||
if (this.overlayTimelineWindow) {
|
||||
return this.overlayTimelineWindow.paginate(EventTimeline.FORWARDS, 1, false);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.timelineWindow!.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
@@ -803,14 +744,11 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
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<IProps, IState> {
|
||||
});
|
||||
}
|
||||
|
||||
private async extendOverlayWindowToCoverMainWindow(): Promise<void> {
|
||||
const mainWindow = this.timelineWindow!;
|
||||
const overlayWindow = this.overlayTimelineWindow!;
|
||||
const mainEvents = mainWindow.getEvents();
|
||||
|
||||
if (mainEvents.length > 0) {
|
||||
let paginationRequests: Promise<unknown>[];
|
||||
|
||||
// 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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
|
||||
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<IProps, IState> {
|
||||
// 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<IProps, IState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise<void> => {
|
||||
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<IProps, IState> {
|
||||
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<IState, "events" | "liveEvents"> {
|
||||
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();
|
||||
|
@@ -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<string | null> {
|
||||
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<string | null> {
|
||||
const existingDMRoom = findDMForUser(client, userId);
|
||||
let roomId: string | null;
|
||||
|
@@ -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",
|
||||
|
@@ -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<EmptyObject> implem
|
||||
}
|
||||
|
||||
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
||||
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
|
||||
}
|
||||
|
@@ -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<void> {
|
||||
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;
|
||||
|
@@ -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<void> {
|
||||
return new Promise<void>((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,
|
||||
protocol: "m.id.phone",
|
||||
fields: {
|
||||
is_native: true,
|
||||
lookup_success: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
} 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,
|
||||
},
|
||||
protocol: "m.id.phone",
|
||||
fields: {},
|
||||
},
|
||||
]);
|
||||
}
|
||||
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);
|
||||
|
||||
|
@@ -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());
|
||||
|
@@ -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());
|
||||
|
@@ -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(<TimelinePanel {...props} />);
|
||||
|
||||
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(
|
||||
<TimelinePanel
|
||||
{...getProps(room, events)}
|
||||
overlayTimelineSet={overlayTimelineSet}
|
||||
overlayTimelineSetFilter={isCallEvent}
|
||||
/>,
|
||||
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(
|
||||
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
|
||||
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(
|
||||
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
|
||||
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(
|
||||
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
|
||||
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(
|
||||
<TimelinePanel
|
||||
{...getProps(room, events)}
|
||||
timelineSet={timelineSet}
|
||||
overlayTimelineSet={overlayTimelineSet}
|
||||
/>,
|
||||
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(
|
||||
<TimelinePanel {...getProps(room, events)} overlayTimelineSet={overlayTimelineSet} />,
|
||||
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", () => {
|
||||
|
@@ -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`] = `
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
!18:example.org
|
||||
!16:example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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);
|
||||
});
|
||||
|
Reference in New Issue
Block a user