Update recent list.

- Don't use deprecated `groupCallEventHander` anymore (it used the old
  `m.call` state event.)
- make the recent list reactive (getting removed from a call removes the
  item from the list)
- support having rooms without shared secret but actual matrix
  encryption in the recent list
- change the share link creation button so that we create a link with
  pwd for sharedKey rooms and with `perParticipantE2EE=true` for matrix
  encrypted rooms.

Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Timo K
2024-04-02 23:57:50 +02:00
committed by Andrew Ferrazzutti
parent 69deece0a3
commit 5bab9b0858
4 changed files with 89 additions and 67 deletions

View File

@@ -20,7 +20,8 @@ import { useLocation } from "react-router-dom";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
export const PASSWORD_STRING = "password="; export const PASSWORD_STRING = "password=";
export const PER_PARTICIPANT_STRING = "perParticipantE2EE=";
export const VIA_SERVERS_STRING = "viaServers=";
interface RoomIdentifier { interface RoomIdentifier {
roomAlias: string | null; roomAlias: string | null;
roomId: string | null; roomId: string | null;

View File

@@ -26,7 +26,7 @@ import styles from "./CallList.module.css";
import { getAbsoluteRoomUrl, getRelativeRoomUrl } from "../matrix-utils"; import { getAbsoluteRoomUrl, getRelativeRoomUrl } from "../matrix-utils";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
import { GroupCallRoom } from "./useGroupCallRooms"; import { GroupCallRoom } from "./useGroupCallRooms";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
interface CallListProps { interface CallListProps {
rooms: GroupCallRoom[]; rooms: GroupCallRoom[];
@@ -66,16 +66,11 @@ interface CallTileProps {
} }
const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => { const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
const roomSharedKey = useRoomSharedKey(room.roomId); const roomEncryptionSystem = useRoomEncryptionSystem(room.roomId);
return ( return (
<div className={styles.callTile}> <div className={styles.callTile}>
<Link <Link
to={getRelativeRoomUrl( to={getRelativeRoomUrl(room.roomId, roomEncryptionSystem, room.name)}
room.roomId,
room.name,
roomSharedKey ?? undefined,
)}
className={styles.callTileLink} className={styles.callTileLink}
> >
<Avatar id={room.roomId} name={name} size={Size.LG} src={avatarUrl} /> <Avatar id={room.roomId} name={name} size={Size.LG} src={avatarUrl} />
@@ -89,11 +84,8 @@ const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
<CopyButton <CopyButton
className={styles.copyButton} className={styles.copyButton}
variant="icon" variant="icon"
value={getAbsoluteRoomUrl( // Todo add the viaServers to the created link
room.roomId, value={getAbsoluteRoomUrl(room.roomId, roomEncryptionSystem, room.name)}
room.name,
roomSharedKey ?? undefined,
)}
/> />
</div> </div>
); );

View File

@@ -15,20 +15,22 @@ limitations under the License.
*/ */
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { EventTimeline, EventType, JoinRule } from "matrix-js-sdk";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { getKeyForRoom, isRoomE2EE } from "../e2ee/sharedKeyManagement"; import { getKeyForRoom } from "../e2ee/sharedKeyManagement";
export interface GroupCallRoom { export interface GroupCallRoom {
roomAlias?: string; roomAlias?: string;
roomName: string; roomName: string;
avatarUrl: string; avatarUrl: string;
room: Room; room: Room;
groupCall: GroupCall; session: MatrixRTCSession;
participants: RoomMember[]; participants: RoomMember[];
} }
const tsCache: { [index: string]: number } = {}; const tsCache: { [index: string]: number } = {};
@@ -46,7 +48,7 @@ function getLastTs(client: MatrixClient, r: Room): number {
const myUserId = client.getUserId()!; const myUserId = client.getUserId()!;
if (r.getMyMembership() !== "join") { if (r.getMyMembership() !== KnownMembership.Join) {
const membershipEvent = r.currentState.getStateEvents( const membershipEvent = r.currentState.getStateEvents(
"m.room.member", "m.room.member",
myUserId, myUserId,
@@ -80,38 +82,51 @@ function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
}); });
} }
function roomIsJoinable(room: Room): boolean { const roomIsJoinable = (room: Room): boolean => {
if (isRoomE2EE(room)) { if (!room.hasEncryptionStateEvent() && !getKeyForRoom(room.roomId)) {
return Boolean(getKeyForRoom(room.roomId)); // if we have an non encrypted room (no encryption state event) we need a locally stored shared key.
} else { // in case this key also does not exists we cannot join the room.
return true; return false;
} }
} // otherwise we can always join rooms because we will automatically decide if we want to use perParticipant or password
const joinRule = room.getJoinRule();
return joinRule === JoinRule.Knock || joinRule === JoinRule.Public;
};
const roomIsJoinedWithCall = (room: Room): boolean => {
const roomStateEvents = room
.getLiveTimeline()
.getState(EventTimeline.FORWARDS)?.events;
return (
room.getMyMembership() === KnownMembership.Join &&
!!roomStateEvents?.get(EventType.GroupCallMemberPrefix)
);
};
export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const [rooms, setRooms] = useState<GroupCallRoom[]>([]); const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
useEffect(() => { useEffect(() => {
function updateRooms(): void { function updateRooms(): void {
if (!client.groupCallEventHandler) { const rooms = client
return; .getRooms()
} .filter(roomIsJoinedWithCall)
const groupCalls = client.groupCallEventHandler.groupCalls.values();
const rooms = Array.from(groupCalls)
.map((groupCall) => groupCall.room)
.filter(roomIsJoinable); .filter(roomIsJoinable);
const sortedRooms = sortRooms(client, rooms); const sortedRooms = sortRooms(client, rooms);
const items = sortedRooms.map((room) => { const items = sortedRooms.map((room) => {
const groupCall = client.getGroupCallForRoom(room.roomId)!; // const groupCall = client.getGroupCallForRoom(room.roomId)!;
const session = client.matrixRTC.getRoomSession(room);
session.memberships;
return { return {
roomAlias: room.getCanonicalAlias() ?? undefined, roomAlias: room.getCanonicalAlias() ?? undefined,
roomName: room.name, roomName: room.name,
avatarUrl: room.getMxcAvatarUrl()!, avatarUrl: room.getMxcAvatarUrl()!,
room, room,
groupCall, session,
participants: [...groupCall!.participants.keys()], participants: session.memberships
.filter((m) => m.sender)
.map((m) => room.getMember(m.sender!))
.filter((m) => m) as RoomMember[],
}; };
}); });
@@ -120,15 +135,17 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
updateRooms(); updateRooms();
client.on(GroupCallEventHandlerEvent.Incoming, updateRooms); client.matrixRTC.on(
client.on(GroupCallEventHandlerEvent.Participants, updateRooms); MatrixRTCSessionManagerEvents.SessionStarted,
updateRooms,
);
client.on(RoomEvent.MyMembership, updateRooms);
return () => { return () => {
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms); client.matrixRTC.off(
client.removeListener( MatrixRTCSessionManagerEvents.SessionStarted,
GroupCallEventHandlerEvent.Participants,
updateRooms, updateRooms,
); );
client.off(RoomEvent.MyMembership, updateRooms);
}; };
}, [client]); }, [client]);

View File

@@ -24,20 +24,21 @@ import { ClientEvent } from "matrix-js-sdk/src/client";
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials"; import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import {
GroupCallIntent,
GroupCallType,
} from "matrix-js-sdk/src/webrtc/groupCall";
import { secureRandomBase64Url } from "matrix-js-sdk/src/randomstring"; import { secureRandomBase64Url } from "matrix-js-sdk/src/randomstring";
import type { MatrixClient } from "matrix-js-sdk/src/client"; import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room"; import type { Room } from "matrix-js-sdk/src/models/room";
import IndexedDBWorker from "./IndexedDBWorker?worker"; import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getUrlParams, PASSWORD_STRING } from "./UrlParams"; import {
getUrlParams,
PASSWORD_STRING,
PER_PARTICIPANT_STRING,
VIA_SERVERS_STRING,
} from "./UrlParams";
import { loadOlm } from "./olm"; import { loadOlm } from "./olm";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { E2eeType } from "./e2ee/e2eeType"; import { E2eeType } from "./e2ee/e2eeType";
import { saveKeyForRoom } from "./e2ee/sharedKeyManagement"; import { EncryptionSystem, saveKeyForRoom } from "./e2ee/sharedKeyManagement";
export const fallbackICEServerAllowed = export const fallbackICEServerAllowed =
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
@@ -340,14 +341,6 @@ export async function createRoom(
logger.log(`Creating group call in ${result.room_id}`); logger.log(`Creating group call in ${result.room_id}`);
await client.createGroupCall(
result.room_id,
GroupCallType.Video,
false,
GroupCallIntent.Room,
true,
);
let password; let password;
if (e2ee == E2eeType.SHARED_KEY) { if (e2ee == E2eeType.SHARED_KEY) {
password = secureRandomBase64Url(16); password = secureRandomBase64Url(16);
@@ -365,39 +358,58 @@ export async function createRoom(
* Returns an absolute URL to that will load Element Call with the given room * Returns an absolute URL to that will load Element Call with the given room
* @param roomId ID of the room * @param roomId ID of the room
* @param roomName Name of the room * @param roomName Name of the room
* @param password e2e key for the room * @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
*/ */
export function getAbsoluteRoomUrl( export function getAbsoluteRoomUrl(
roomId: string, roomId: string,
encryptionSystem: EncryptionSystem,
roomName?: string, roomName?: string,
password?: string, viaServers?: string[],
): string { ): string {
return `${window.location.protocol}//${ return `${window.location.protocol}//${
window.location.host window.location.host
}${getRelativeRoomUrl(roomId, roomName, password)}`; }${getRelativeRoomUrl(roomId, encryptionSystem, roomName, viaServers)}`;
} }
/** /**
* Returns a relative URL to that will load Element Call with the given room * Returns a relative URL to that will load Element Call with the given room
* @param roomId ID of the room * @param roomId ID of the room
* @param roomName Name of the room * @param roomName Name of the room
* @param password e2e key for the room * @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
*/ */
export function getRelativeRoomUrl( export function getRelativeRoomUrl(
roomId: string, roomId: string,
encryptionSystem: EncryptionSystem,
roomName?: string, roomName?: string,
password?: string, viaServers?: string[],
): string { ): string {
// The password shouldn't need URL encoding here (we generate URL-safe ones) but encode // The password shouldn't need URL encoding here (we generate URL-safe ones) but encode
// it in case it came from another client that generated a non url-safe one // it in case it came from another client that generated a non url-safe one
const encodedPassword = password ? encodeURIComponent(password) : undefined; const encryptionPart = ((): string => {
if (password && encodedPassword !== password) { switch (encryptionSystem?.kind) {
logger.info("Encoded call password used non URL-safe chars: buggy client?"); case E2eeType.SHARED_KEY: {
} const encodedPassword = encodeURIComponent(encryptionSystem.secret);
if (encodedPassword !== encryptionSystem.secret) {
logger.info(
"Encoded call password used non URL-safe chars: buggy client?",
);
}
return "&" + PASSWORD_STRING + encodedPassword;
}
case E2eeType.PER_PARTICIPANT:
return "&" + PER_PARTICIPANT_STRING + "true";
case E2eeType.NONE:
return "";
}
})();
const roomIdPart = `roomId=${roomId}`;
const viaServersPart = viaServers
? viaServers.map((s) => "&" + VIA_SERVERS_STRING + s).join("")
: "";
return `/room/#${ return `/room/#${
roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : "" roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : ""
}?roomId=${roomId}${password ? "&" + PASSWORD_STRING + encodedPassword : ""}`; }?${roomIdPart}${encryptionPart}${viaServersPart}`;
} }
export function getAvatarUrl( export function getAvatarUrl(