/* Copyright 2022-2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { type MatrixClient, type RoomMember, type Room, RoomEvent, EventTimeline, EventType, JoinRule, KnownMembership, } from "matrix-js-sdk"; import { useState, useEffect } from "react"; import { MatrixRTCSessionManagerEvents, type MatrixRTCSession, } from "matrix-js-sdk/lib/matrixrtc"; import { logger } from "matrix-js-sdk/lib/logger"; import { getKeyForRoom } from "../e2ee/sharedKeyManagement"; export interface GroupCallRoom { roomAlias?: string; roomName: string; avatarUrl: string; room: Room; session: MatrixRTCSession; participants: RoomMember[]; } const tsCache: { [index: string]: number } = {}; function getLastTs(client: MatrixClient, r: Room): number { if (tsCache[r.roomId]) { return tsCache[r.roomId]; } if (!r || !r.timeline) { const ts = Number.MAX_SAFE_INTEGER; tsCache[r.roomId] = ts; return ts; } const myUserId = client.getUserId()!; if (r.getMyMembership() !== KnownMembership.Join) { const membershipEvent = r.currentState.getStateEvents( "m.room.member", myUserId, ); if (membershipEvent && !Array.isArray(membershipEvent)) { const ts = membershipEvent.getTs(); tsCache[r.roomId] = ts; return ts; } } for (let i = r.timeline.length - 1; i >= 0; --i) { const ev = r.timeline[i]; const ts = ev.getTs(); if (ts) { tsCache[r.roomId] = ts; return ts; } } const ts = Number.MAX_SAFE_INTEGER; tsCache[r.roomId] = ts; return ts; } function sortRooms(client: MatrixClient, rooms: Room[]): Room[] { return rooms.sort((a, b) => { return getLastTs(client, b) - getLastTs(client, a); }); } const roomIsJoinable = (room: Room): boolean => { const password = getKeyForRoom(room.roomId); if (!room.hasEncryptionStateEvent() && !password) { // if we have a non encrypted room (no encryption state event) we need a locally stored shared key. // in case this key also does not exists we cannot join the room. return false; } // otherwise we can always join rooms because we will automatically decide if we want to use perParticipant or password switch (room.getJoinRule()) { case JoinRule.Public: return true; case JoinRule.Knock: switch (room.getMyMembership()) { case KnownMembership.Join: case KnownMembership.Knock: return true; case KnownMembership.Invite: return ( room .getLiveTimeline() .getState(EventTimeline.FORWARDS) ?.getStateEvents(EventType.RoomMember, room.myUserId) ?.getPrevContent().membership === JoinRule.Knock ); default: return false; } // TODO: check JoinRule.Restricted and return true if join condition is satisfied default: return room.getMyMembership() === KnownMembership.Join; } }; const roomHasCallMembershipEvents = (room: Room): boolean => { switch (room.getMyMembership()) { case KnownMembership.Join: return !!room .getLiveTimeline() .getState(EventTimeline.FORWARDS) ?.events?.get(EventType.GroupCallMemberPrefix); case KnownMembership.Knock: // Assume that a room you've knocked on is able to hold calls return true; default: return false; } }; export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { const [rooms, setRooms] = useState([]); useEffect(() => { function updateRooms(): void { // We want to show all rooms that historically had a call and which we are (or can become) part of. const rooms = client .getRooms() .filter(roomHasCallMembershipEvents) .filter(roomIsJoinable); const sortedRooms = sortRooms(client, rooms); Promise.all( sortedRooms.map(async (room) => { const session = await client.matrixRTC.getRoomSession(room); return { roomAlias: room.getCanonicalAlias() ?? undefined, roomName: room.name, avatarUrl: room.getMxcAvatarUrl()!, room, session, participants: session.memberships .filter((m) => m.sender) .map((m) => room.getMember(m.sender!)) .filter((m) => m) as RoomMember[], }; }), ) .then((items) => setRooms(items)) .catch(logger.error); } updateRooms(); client.matrixRTC.on( MatrixRTCSessionManagerEvents.SessionStarted, updateRooms, ); client.on(RoomEvent.MyMembership, updateRooms); return (): void => { client.matrixRTC.off( MatrixRTCSessionManagerEvents.SessionStarted, updateRooms, ); client.off(RoomEvent.MyMembership, updateRooms); }; }, [client]); return rooms; }