mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
Merge branch 'livekit' into layout-state
This commit is contained in:
commit
c5e60744a2
4
.github/workflows/docker.yaml
vendored
4
.github/workflows/docker.yaml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
run-id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
name: build-output
|
||||
path: dist
|
||||
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@a530e948adbeb357dbca95a7f8845d385edf4438
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@7e6f77677b7892794c8852c6e3773c3e9bc3129a
|
||||
uses: docker/build-push-action@eb539f44b153603ccbfbd98e2ab9d4d0dcaf23a4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
9
.github/workflows/publish.yaml
vendored
9
.github/workflows/publish.yaml
vendored
@ -15,7 +15,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build_element_call:
|
||||
if: ${{ github.event.workflow_run.event == 'release' }}
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: ./.github/workflows/element-call.yaml
|
||||
with:
|
||||
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
||||
@ -25,6 +25,8 @@ jobs:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
publish_tarball:
|
||||
needs: build_element_call
|
||||
if: always()
|
||||
name: Publish tarball
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
@ -40,7 +42,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
run-id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
name: build-output
|
||||
path: dist
|
||||
- name: Create Tarball
|
||||
@ -49,13 +51,14 @@ jobs:
|
||||
run: |
|
||||
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@b06cde36fc32a3ee94080e87258567f73f921537
|
||||
uses: actions/upload-artifact@552bf3722c16e81001aea7db72d8cedf64eb5f68
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
path: "./element-call-*.tar.gz"
|
||||
publish_docker:
|
||||
needs: publish_tarball
|
||||
if: always()
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
2
.github/workflows/translations-download.yaml
vendored
2
.github/workflows/translations-download.yaml
vendored
@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v6.0.3
|
||||
uses: peter-evans/create-pull-request@v6.0.5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/localazy-download
|
||||
|
@ -1,5 +1,4 @@
|
||||
port: 7880
|
||||
environment: dev
|
||||
bind_addresses:
|
||||
- "0.0.0.0"
|
||||
rtc:
|
||||
|
@ -62,7 +62,7 @@
|
||||
"i18next-http-backend": "^2.0.0",
|
||||
"livekit-client": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d55c6a36df539f6adacc335efe5b9be27c9cee4a",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e874468ba3e84819cf4b342d2e66af67ab4cf804",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pako": "^2.0.4",
|
||||
|
@ -60,8 +60,17 @@
|
||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||
"full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.</0>",
|
||||
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
|
||||
"group_call_loader_failed_heading": "Call not found",
|
||||
"group_call_loader_failed_text": "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.",
|
||||
"group_call_loader": {
|
||||
"banned_body": "You have been banned from the room.",
|
||||
"banned_heading": "Banned",
|
||||
"call_ended_body": "You have been removed from the call.",
|
||||
"call_ended_heading": "Call ended",
|
||||
"failed_heading": "Call not found",
|
||||
"failed_text": "Calls are now end-to-end encrypted and need to be created from the home page. This helps make sure everyone's using the same encryption key.",
|
||||
"knock_reject_body": "The room members declined your request to join.",
|
||||
"knock_reject_heading": "Not allowed to join",
|
||||
"reason": "Reason"
|
||||
},
|
||||
"hangup_button_label": "End call",
|
||||
"header_label": "Element Call Home",
|
||||
"header_participants_label": "Participants",
|
||||
@ -77,8 +86,10 @@
|
||||
"layout_grid_label": "Grid",
|
||||
"layout_spotlight_label": "Spotlight",
|
||||
"lobby": {
|
||||
"ask_to_join": "Ask to join call",
|
||||
"join_button": "Join call",
|
||||
"leave_button": "Back to recents"
|
||||
"leave_button": "Back to recents",
|
||||
"waiting_for_invite": "Request sent"
|
||||
},
|
||||
"log_in": "Log In",
|
||||
"logging_in": "Logging in…",
|
||||
|
@ -58,6 +58,7 @@ interface ErrorViewProps {
|
||||
|
||||
export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||
const location = useLocation();
|
||||
const { confineToRoom } = useUrlParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
@ -78,25 +79,26 @@ export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||
: error.message}
|
||||
</p>
|
||||
<RageshakeButton description={`***Error View***: ${error.message}`} />
|
||||
{location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
)}
|
||||
{!confineToRoom &&
|
||||
(location.pathname === "/" ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
onPress={onReload}
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</Button>
|
||||
) : (
|
||||
<LinkButton
|
||||
size="lg"
|
||||
variant="default"
|
||||
className={styles.homeLink}
|
||||
to="/"
|
||||
>
|
||||
{t("return_home_button")}
|
||||
</LinkButton>
|
||||
))}
|
||||
</FullScreenView>
|
||||
);
|
||||
};
|
||||
|
@ -117,7 +117,7 @@ interface RoomHeaderInfoProps {
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
encrypted: boolean;
|
||||
participantCount: number;
|
||||
participantCount: number | null;
|
||||
}
|
||||
|
||||
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
||||
@ -150,7 +150,7 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
||||
</Heading>
|
||||
<EncryptionLock encrypted={encrypted} />
|
||||
</div>
|
||||
{participantCount > 0 && (
|
||||
{(participantCount ?? 0) > 0 && (
|
||||
<div className={styles.participantsLine}>
|
||||
<UserProfileIcon
|
||||
width={20}
|
||||
@ -158,7 +158,7 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
||||
aria-label={t("header_participants_label")}
|
||||
/>
|
||||
<Text as="span" size="sm" weight="medium">
|
||||
{t("participant_count", { count: participantCount })}
|
||||
{t("participant_count", { count: participantCount ?? 0 })}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
@ -16,10 +16,11 @@ limitations under the License.
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
export const PASSWORD_STRING = "password=";
|
||||
import { EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
|
||||
interface RoomIdentifier {
|
||||
roomAlias: string | null;
|
||||
@ -328,3 +329,32 @@ export const useRoomIdentifier = (): RoomIdentifier => {
|
||||
[pathname, search, hash],
|
||||
);
|
||||
};
|
||||
|
||||
export function generateUrlSearchParams(
|
||||
roomId: string,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
viaServers?: string[],
|
||||
): URLSearchParams {
|
||||
const params = new URLSearchParams();
|
||||
// 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
|
||||
switch (encryptionSystem?.kind) {
|
||||
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?",
|
||||
);
|
||||
}
|
||||
params.set("password", encodedPassword);
|
||||
break;
|
||||
}
|
||||
case E2eeType.PER_PARTICIPANT:
|
||||
params.set("perParticipantE2EE", "true");
|
||||
break;
|
||||
}
|
||||
params.set("roomId", roomId);
|
||||
viaServers?.forEach((s) => params.set("viaServers", s));
|
||||
|
||||
return params;
|
||||
}
|
||||
|
@ -15,12 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Room } from "matrix-js-sdk";
|
||||
|
||||
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
|
||||
import { widget } from "../widget";
|
||||
import { E2eeType } from "./e2eeType";
|
||||
import { useClient } from "../ClientContext";
|
||||
|
||||
export function saveKeyForRoom(roomId: string, password: string): void {
|
||||
setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password);
|
||||
@ -68,30 +67,37 @@ const useKeyFromUrl = (): [string, string] | [undefined, undefined] => {
|
||||
: [undefined, undefined];
|
||||
};
|
||||
|
||||
export const useRoomSharedKey = (roomId: string): string | undefined => {
|
||||
export type Unencrypted = { kind: E2eeType.NONE };
|
||||
export type SharedSecret = { kind: E2eeType.SHARED_KEY; secret: string };
|
||||
export type PerParticipantE2EE = { kind: E2eeType.PER_PARTICIPANT };
|
||||
export type EncryptionSystem = Unencrypted | SharedSecret | PerParticipantE2EE;
|
||||
|
||||
export function useRoomEncryptionSystem(roomId: string): EncryptionSystem {
|
||||
const { client } = useClient();
|
||||
|
||||
// make sure we've extracted the key from the URL first
|
||||
// (and we still need to take the value it returns because
|
||||
// the effect won't run in time for it to save to localstorage in
|
||||
// time for us to read it out again).
|
||||
const [urlRoomId, passwordFormUrl] = useKeyFromUrl();
|
||||
|
||||
const [urlRoomId, passwordFromUrl] = useKeyFromUrl();
|
||||
const storedPassword = useInternalRoomSharedKey(roomId);
|
||||
|
||||
if (storedPassword) return storedPassword;
|
||||
if (urlRoomId === roomId) return passwordFormUrl;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const useIsRoomE2EE = (roomId: string): boolean | null => {
|
||||
const { client } = useClient();
|
||||
const room = useMemo(() => client?.getRoom(roomId), [roomId, client]);
|
||||
|
||||
return useMemo(() => !room || isRoomE2EE(room), [room]);
|
||||
};
|
||||
|
||||
export function isRoomE2EE(room: Room): boolean {
|
||||
// For now, rooms in widget mode are never considered encrypted.
|
||||
// In the future, when widget mode gains encryption support, then perhaps we
|
||||
// should inspect the e2eEnabled URL parameter here?
|
||||
return widget === null && !room.getCanonicalAlias();
|
||||
const room = client?.getRoom(roomId);
|
||||
const e2eeSystem = <EncryptionSystem>useMemo(() => {
|
||||
if (!room) return { kind: E2eeType.NONE };
|
||||
if (storedPassword)
|
||||
return {
|
||||
kind: E2eeType.SHARED_KEY,
|
||||
secret: storedPassword,
|
||||
};
|
||||
if (urlRoomId === roomId)
|
||||
return {
|
||||
kind: E2eeType.SHARED_KEY,
|
||||
secret: passwordFromUrl,
|
||||
};
|
||||
if (room.hasEncryptionStateEvent()) {
|
||||
return { kind: E2eeType.PER_PARTICIPANT };
|
||||
}
|
||||
return { kind: E2eeType.NONE };
|
||||
}, [passwordFromUrl, room, roomId, storedPassword, urlRoomId]);
|
||||
return e2eeSystem;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import styles from "./CallList.module.css";
|
||||
import { getAbsoluteRoomUrl, getRelativeRoomUrl } from "../matrix-utils";
|
||||
import { Body } from "../typography/Typography";
|
||||
import { GroupCallRoom } from "./useGroupCallRooms";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface CallListProps {
|
||||
rooms: GroupCallRoom[];
|
||||
@ -66,16 +66,11 @@ interface CallTileProps {
|
||||
}
|
||||
|
||||
const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
|
||||
const roomSharedKey = useRoomSharedKey(room.roomId);
|
||||
|
||||
const roomEncryptionSystem = useRoomEncryptionSystem(room.roomId);
|
||||
return (
|
||||
<div className={styles.callTile}>
|
||||
<Link
|
||||
to={getRelativeRoomUrl(
|
||||
room.roomId,
|
||||
room.name,
|
||||
roomSharedKey ?? undefined,
|
||||
)}
|
||||
to={getRelativeRoomUrl(room.roomId, roomEncryptionSystem, room.name)}
|
||||
className={styles.callTileLink}
|
||||
>
|
||||
<Avatar id={room.roomId} name={name} size={Size.LG} src={avatarUrl} />
|
||||
@ -89,11 +84,8 @@ const CallTile: FC<CallTileProps> = ({ name, avatarUrl, room }) => {
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
variant="icon"
|
||||
value={getAbsoluteRoomUrl(
|
||||
room.roomId,
|
||||
room.name,
|
||||
roomSharedKey ?? undefined,
|
||||
)}
|
||||
// Todo add the viaServers to the created link
|
||||
value={getAbsoluteRoomUrl(room.roomId, roomEncryptionSystem, room.name)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -78,12 +78,14 @@ export const RegisteredView: FC<Props> = ({ client }) => {
|
||||
roomName,
|
||||
E2eeType.SHARED_KEY,
|
||||
);
|
||||
if (!createRoomResult.password)
|
||||
throw new Error("Failed to create room with shared secret");
|
||||
|
||||
history.push(
|
||||
getRelativeRoomUrl(
|
||||
createRoomResult.roomId,
|
||||
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
|
||||
roomName,
|
||||
createRoomResult.password,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -116,13 +116,15 @@ export const UnauthenticatedView: FC = () => {
|
||||
if (!setClient) {
|
||||
throw new Error("setClient is undefined");
|
||||
}
|
||||
if (!createRoomResult.password)
|
||||
throw new Error("Failed to create room with shared secret");
|
||||
|
||||
setClient({ client, session });
|
||||
history.push(
|
||||
getRelativeRoomUrl(
|
||||
createRoomResult.roomId,
|
||||
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
|
||||
roomName,
|
||||
createRoomResult.password,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -15,20 +15,22 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
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 { 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 {
|
||||
roomAlias?: string;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
room: Room;
|
||||
groupCall: GroupCall;
|
||||
session: MatrixRTCSession;
|
||||
participants: RoomMember[];
|
||||
}
|
||||
const tsCache: { [index: string]: number } = {};
|
||||
@ -46,7 +48,7 @@ function getLastTs(client: MatrixClient, r: Room): number {
|
||||
|
||||
const myUserId = client.getUserId()!;
|
||||
|
||||
if (r.getMyMembership() !== "join") {
|
||||
if (r.getMyMembership() !== KnownMembership.Join) {
|
||||
const membershipEvent = r.currentState.getStateEvents(
|
||||
"m.room.member",
|
||||
myUserId,
|
||||
@ -80,38 +82,51 @@ function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
|
||||
});
|
||||
}
|
||||
|
||||
function roomIsJoinable(room: Room): boolean {
|
||||
if (isRoomE2EE(room)) {
|
||||
return Boolean(getKeyForRoom(room.roomId));
|
||||
} else {
|
||||
return true;
|
||||
const roomIsJoinable = (room: Room): boolean => {
|
||||
if (!room.hasEncryptionStateEvent() && !getKeyForRoom(room.roomId)) {
|
||||
// if we have an 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
|
||||
const joinRule = room.getJoinRule();
|
||||
return joinRule === JoinRule.Knock || joinRule === JoinRule.Public;
|
||||
};
|
||||
|
||||
const roomHasCallMembershipEvents = (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[] {
|
||||
const [rooms, setRooms] = useState<GroupCallRoom[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
function updateRooms(): void {
|
||||
if (!client.groupCallEventHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupCalls = client.groupCallEventHandler.groupCalls.values();
|
||||
const rooms = Array.from(groupCalls)
|
||||
.map((groupCall) => groupCall.room)
|
||||
// We want to show all rooms that historically had a call and which we are (can become) part of.
|
||||
const rooms = client
|
||||
.getRooms()
|
||||
.filter(roomHasCallMembershipEvents)
|
||||
.filter(roomIsJoinable);
|
||||
const sortedRooms = sortRooms(client, rooms);
|
||||
const items = sortedRooms.map((room) => {
|
||||
const groupCall = client.getGroupCallForRoom(room.roomId)!;
|
||||
|
||||
const session = client.matrixRTC.getRoomSession(room);
|
||||
session.memberships;
|
||||
return {
|
||||
roomAlias: room.getCanonicalAlias() ?? undefined,
|
||||
roomName: room.name,
|
||||
avatarUrl: room.getMxcAvatarUrl()!,
|
||||
room,
|
||||
groupCall,
|
||||
participants: [...groupCall!.participants.keys()],
|
||||
session,
|
||||
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();
|
||||
|
||||
client.on(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||
client.on(GroupCallEventHandlerEvent.Participants, updateRooms);
|
||||
|
||||
client.matrixRTC.on(
|
||||
MatrixRTCSessionManagerEvents.SessionStarted,
|
||||
updateRooms,
|
||||
);
|
||||
client.on(RoomEvent.MyMembership, updateRooms);
|
||||
return () => {
|
||||
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||
client.removeListener(
|
||||
GroupCallEventHandlerEvent.Participants,
|
||||
client.matrixRTC.off(
|
||||
MatrixRTCSessionManagerEvents.SessionStarted,
|
||||
updateRooms,
|
||||
);
|
||||
client.off(RoomEvent.MyMembership, updateRooms);
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
|
@ -41,11 +41,7 @@ import {
|
||||
} from "./useECConnectionState";
|
||||
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
|
||||
export type E2EEConfig = {
|
||||
mode: E2eeType;
|
||||
sharedKey?: string;
|
||||
};
|
||||
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface UseLivekitResult {
|
||||
livekitRoom?: Room;
|
||||
@ -56,41 +52,35 @@ export function useLiveKit(
|
||||
rtcSession: MatrixRTCSession,
|
||||
muteStates: MuteStates,
|
||||
sfuConfig: SFUConfig | undefined,
|
||||
e2eeConfig: E2EEConfig,
|
||||
e2eeSystem: EncryptionSystem,
|
||||
): UseLivekitResult {
|
||||
const e2eeOptions = useMemo((): E2EEOptions | undefined => {
|
||||
if (e2eeConfig.mode === E2eeType.NONE) return undefined;
|
||||
if (e2eeSystem.kind === E2eeType.NONE) return undefined;
|
||||
|
||||
if (e2eeConfig.mode === E2eeType.PER_PARTICIPANT) {
|
||||
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
|
||||
return {
|
||||
keyProvider: new MatrixKeyProvider(),
|
||||
worker: new E2EEWorker(),
|
||||
};
|
||||
} else if (
|
||||
e2eeConfig.mode === E2eeType.SHARED_KEY &&
|
||||
e2eeConfig.sharedKey
|
||||
) {
|
||||
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
|
||||
return {
|
||||
keyProvider: new ExternalE2EEKeyProvider(),
|
||||
worker: new E2EEWorker(),
|
||||
};
|
||||
}
|
||||
}, [e2eeConfig]);
|
||||
}, [e2eeSystem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (e2eeConfig.mode === E2eeType.NONE || !e2eeOptions) return;
|
||||
if (e2eeSystem.kind === E2eeType.NONE || !e2eeOptions) return;
|
||||
|
||||
if (e2eeConfig.mode === E2eeType.PER_PARTICIPANT) {
|
||||
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
|
||||
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
|
||||
} else if (
|
||||
e2eeConfig.mode === E2eeType.SHARED_KEY &&
|
||||
e2eeConfig.sharedKey
|
||||
) {
|
||||
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
|
||||
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
|
||||
e2eeConfig.sharedKey,
|
||||
e2eeSystem.secret,
|
||||
);
|
||||
}
|
||||
}, [e2eeOptions, e2eeConfig, rtcSession]);
|
||||
}, [e2eeOptions, e2eeSystem, rtcSession]);
|
||||
|
||||
const initialMuteStates = useRef<MuteStates>(muteStates);
|
||||
const devices = useMediaDevices();
|
||||
@ -131,9 +121,9 @@ export function useLiveKit(
|
||||
// useEffect() with an argument that references itself, if E2EE is enabled
|
||||
const room = useMemo(() => {
|
||||
const r = new Room(roomOptions);
|
||||
r.setE2EEEnabled(e2eeConfig.mode !== E2eeType.NONE);
|
||||
r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE);
|
||||
return r;
|
||||
}, [roomOptions, e2eeConfig]);
|
||||
}, [roomOptions, e2eeSystem]);
|
||||
|
||||
const connectionState = useECConnectionState(
|
||||
{
|
||||
|
@ -19,25 +19,25 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||
import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
createClient,
|
||||
ICreateClientOpts,
|
||||
Preset,
|
||||
Visibility,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
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 type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||
import { getUrlParams, PASSWORD_STRING } from "./UrlParams";
|
||||
import { generateUrlSearchParams, getUrlParams } from "./UrlParams";
|
||||
import { loadOlm } from "./olm";
|
||||
import { Config } from "./config/Config";
|
||||
import { E2eeType } from "./e2ee/e2eeType";
|
||||
import { saveKeyForRoom } from "./e2ee/sharedKeyManagement";
|
||||
import { EncryptionSystem, saveKeyForRoom } from "./e2ee/sharedKeyManagement";
|
||||
|
||||
export const fallbackICEServerAllowed =
|
||||
import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true";
|
||||
@ -338,16 +338,6 @@ export async function createRoom(
|
||||
|
||||
const result = await createPromise;
|
||||
|
||||
logger.log(`Creating group call in ${result.room_id}`);
|
||||
|
||||
await client.createGroupCall(
|
||||
result.room_id,
|
||||
GroupCallType.Video,
|
||||
false,
|
||||
GroupCallIntent.Room,
|
||||
true,
|
||||
);
|
||||
|
||||
let password;
|
||||
if (e2ee == E2eeType.SHARED_KEY) {
|
||||
password = secureRandomBase64Url(16);
|
||||
@ -365,39 +355,35 @@ export async function createRoom(
|
||||
* Returns an absolute URL to that will load Element Call with the given room
|
||||
* @param roomId ID 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(
|
||||
roomId: string,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
roomName?: string,
|
||||
password?: string,
|
||||
viaServers?: string[],
|
||||
): string {
|
||||
return `${window.location.protocol}//${
|
||||
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
|
||||
* @param roomId ID 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(
|
||||
roomId: string,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
roomName?: string,
|
||||
password?: string,
|
||||
viaServers?: string[],
|
||||
): string {
|
||||
// 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
|
||||
const encodedPassword = password ? encodeURIComponent(password) : undefined;
|
||||
if (password && encodedPassword !== password) {
|
||||
logger.info("Encoded call password used non URL-safe chars: buggy client?");
|
||||
}
|
||||
|
||||
return `/room/#${
|
||||
roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : ""
|
||||
}?roomId=${roomId}${password ? "&" + PASSWORD_STRING + encodedPassword : ""}`;
|
||||
const roomPart = roomName
|
||||
? "/" + roomAliasLocalpartFromRoomName(roomName)
|
||||
: "";
|
||||
return `/room/#${roomPart}?${generateUrlSearchParams(roomId, encryptionSystem, viaServers).toString()}`;
|
||||
}
|
||||
|
||||
export function getAvatarUrl(
|
||||
|
@ -21,13 +21,14 @@ import PopOutIcon from "@vector-im/compound-design-tokens/icons/pop-out.svg?reac
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { Modal } from "../Modal";
|
||||
import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||
import styles from "./AppSelectionModal.module.css";
|
||||
import { editFragmentQuery } from "../UrlParams";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
|
||||
interface Props {
|
||||
roomId: string | null;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
@ -42,10 +43,9 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
},
|
||||
[setOpen],
|
||||
);
|
||||
const e2eeSystem = useRoomEncryptionSystem(roomId);
|
||||
|
||||
const roomSharedKey = useRoomSharedKey(roomId ?? "");
|
||||
const roomIsEncrypted = useIsRoomE2EE(roomId ?? "");
|
||||
if (roomIsEncrypted && roomSharedKey === undefined) {
|
||||
if (e2eeSystem.kind === E2eeType.NONE) {
|
||||
logger.error(
|
||||
"Generating app redirect URL for encrypted room but don't have key available!",
|
||||
);
|
||||
@ -60,7 +60,7 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
const url = new URL(
|
||||
roomId === null
|
||||
? window.location.href
|
||||
: getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined),
|
||||
: getAbsoluteRoomUrl(roomId, e2eeSystem),
|
||||
);
|
||||
// Edit the URL to prevent the app selection prompt from appearing a second
|
||||
// time within the app, and to keep the user confined to the current room
|
||||
@ -73,7 +73,7 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
const result = new URL("io.element.call:/");
|
||||
result.searchParams.set("url", url.toString());
|
||||
return result.toString();
|
||||
}, [roomId, roomSharedKey]);
|
||||
}, [e2eeSystem, roomId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -14,22 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode, useCallback } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { MatrixError } from "matrix-js-sdk";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Heading, Link, Text } from "@vector-im/compound-web";
|
||||
|
||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import {
|
||||
useLoadGroupCall,
|
||||
GroupCallStatus,
|
||||
CallTerminatedMessage,
|
||||
} from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomIdOrAlias: string;
|
||||
viaServers: string[];
|
||||
children: (rtcSession: MatrixRTCSession) => ReactNode;
|
||||
children: (groupCallState: GroupCallStatus) => JSX.Element;
|
||||
}
|
||||
|
||||
export function GroupCallLoader({
|
||||
@ -51,20 +54,22 @@ export function GroupCallLoader({
|
||||
);
|
||||
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
case "waitForInvite":
|
||||
case "canKnock":
|
||||
return children(groupCallState);
|
||||
case "loading":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("common.loading")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
case "loaded":
|
||||
return <>{children(groupCallState.rtcSession)}</>;
|
||||
case "failed":
|
||||
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{t("group_call_loader_failed_heading")}</Heading>
|
||||
<Text>{t("group_call_loader_failed_text")}</Text>
|
||||
<Heading>{t("group_call_loader.failed_heading")}</Heading>
|
||||
<Text>{t("group_call_loader.failed_text")}</Text>
|
||||
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
|
||||
dupes of this flow, let's make a common component and put it here. */}
|
||||
<Link href="/" onClick={onHomeClick}>
|
||||
@ -72,6 +77,22 @@ export function GroupCallLoader({
|
||||
</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else if (groupCallState.error instanceof CallTerminatedMessage) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{groupCallState.error.message}</Heading>
|
||||
<Text>{groupCallState.error.messageBody}</Text>
|
||||
{groupCallState.error.reason && (
|
||||
<>
|
||||
{t("group_call_loader.reason")}:
|
||||
<Text size="sm">"{groupCallState.error.reason}"</Text>
|
||||
</>
|
||||
)}
|
||||
<Link href="/" onClick={onHomeClick}>
|
||||
{t("common.home")}
|
||||
</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
}
|
||||
|
@ -17,7 +17,10 @@ limitations under the License.
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room, isE2EESupported } from "livekit-client";
|
||||
import {
|
||||
Room,
|
||||
isE2EESupported as isE2EESupportedBrowser,
|
||||
} from "livekit-client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||
@ -26,7 +29,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { FullScreenView } from "../FullScreenView";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { MatrixInfo } from "./VideoPreview";
|
||||
import { CallEndedView } from "./CallEndedView";
|
||||
@ -34,17 +37,16 @@ import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import { findDeviceByName } from "../media-utils";
|
||||
import { ActiveCall } from "./InCallView";
|
||||
import { MuteStates, useMuteStates } from "./MuteStates";
|
||||
import { MUTE_PARTICIPANT_COUNT, MuteStates } from "./MuteStates";
|
||||
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
||||
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
||||
import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { E2EEConfig } from "../livekit/useLiveKit";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
|
||||
@ -62,6 +64,7 @@ interface Props {
|
||||
skipLobby: boolean;
|
||||
hideHeader: boolean;
|
||||
rtcSession: MatrixRTCSession;
|
||||
muteStates: MuteStates;
|
||||
}
|
||||
|
||||
export const GroupCallView: FC<Props> = ({
|
||||
@ -72,10 +75,23 @@ export const GroupCallView: FC<Props> = ({
|
||||
skipLobby,
|
||||
hideHeader,
|
||||
rtcSession,
|
||||
muteStates,
|
||||
}) => {
|
||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
||||
|
||||
// The mute state reactively gets updated once the participant count reaches the threshold.
|
||||
// The user then still is able to unmute again.
|
||||
// The more common case is that the user is muted from the start (participant count is already over the threshold).
|
||||
const autoMuteHappened = useRef(false);
|
||||
useEffect(() => {
|
||||
if (autoMuteHappened.current) return;
|
||||
if (memberships.length >= MUTE_PARTICIPANT_COUNT) {
|
||||
muteStates.audio.setEnabled?.(false);
|
||||
autoMuteHappened.current = true;
|
||||
}
|
||||
}, [autoMuteHappened, memberships, muteStates.audio]);
|
||||
|
||||
useEffect(() => {
|
||||
window.rtcSession = rtcSession;
|
||||
return () => {
|
||||
@ -86,10 +102,8 @@ export const GroupCallView: FC<Props> = ({
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const roomName = useRoomName(rtcSession.room);
|
||||
const roomAvatar = useRoomAvatar(rtcSession.room);
|
||||
const e2eeSharedKey = useRoomSharedKey(rtcSession.room.roomId);
|
||||
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
||||
const roomEncrypted =
|
||||
useIsRoomE2EE(rtcSession.room.roomId) || perParticipantE2EE;
|
||||
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
|
||||
|
||||
const matrixInfo = useMemo((): MatrixInfo => {
|
||||
return {
|
||||
@ -100,16 +114,16 @@ export const GroupCallView: FC<Props> = ({
|
||||
roomName,
|
||||
roomAlias: rtcSession.room.getCanonicalAlias(),
|
||||
roomAvatar,
|
||||
roomEncrypted,
|
||||
e2eeSystem,
|
||||
};
|
||||
}, [
|
||||
client,
|
||||
displayName,
|
||||
avatarUrl,
|
||||
rtcSession,
|
||||
rtcSession.room,
|
||||
roomName,
|
||||
roomAvatar,
|
||||
roomEncrypted,
|
||||
client,
|
||||
e2eeSystem,
|
||||
]);
|
||||
|
||||
// Count each member only once, regardless of how many devices they use
|
||||
@ -122,20 +136,9 @@ export const GroupCallView: FC<Props> = ({
|
||||
const latestDevices = useRef<MediaDevices>();
|
||||
latestDevices.current = deviceContext;
|
||||
|
||||
const muteStates = useMuteStates(memberships.length);
|
||||
const latestMuteStates = useRef<MuteStates>();
|
||||
latestMuteStates.current = muteStates;
|
||||
|
||||
const e2eeConfig = useMemo((): E2EEConfig => {
|
||||
if (perParticipantE2EE) {
|
||||
return { mode: E2eeType.PER_PARTICIPANT };
|
||||
} else if (e2eeSharedKey) {
|
||||
return { mode: E2eeType.SHARED_KEY, sharedKey: e2eeSharedKey };
|
||||
} else {
|
||||
return { mode: E2eeType.NONE };
|
||||
}
|
||||
}, [perParticipantE2EE, e2eeSharedKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultDeviceSetup = async (
|
||||
requestedDeviceData: JoinCallData,
|
||||
@ -288,17 +291,8 @@ export const GroupCallView: FC<Props> = ({
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (roomEncrypted && !perParticipantE2EE && !e2eeSharedKey) {
|
||||
return (
|
||||
<ErrorView
|
||||
error={
|
||||
new Error(
|
||||
"No E2EE key provided: please make sure the URL you're using to join this call has been retrieved using the in-app button.",
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (!isE2EESupported() && roomEncrypted) {
|
||||
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
|
||||
// If we have a encryption system but the browser does not support it.
|
||||
return (
|
||||
<FullScreenView>
|
||||
<Heading>{t("browser_media_e2ee_unsupported_heading")}</Heading>
|
||||
@ -345,7 +339,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
onLeave={onLeave}
|
||||
hideHeader={hideHeader}
|
||||
muteStates={muteStates}
|
||||
e2eeConfig={e2eeConfig}
|
||||
e2eeSystem={e2eeSystem}
|
||||
//otelGroupCallMembership={otelGroupCallMembership}
|
||||
onShareClick={onShareClick}
|
||||
/>
|
||||
|
@ -63,7 +63,7 @@ import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
||||
import { useLiveKit } from "../livekit/useLiveKit";
|
||||
import { useFullscreen } from "./useFullscreen";
|
||||
import { useLayoutStates } from "../video-grid/Layout";
|
||||
import { useWakeLock } from "../useWakeLock";
|
||||
@ -76,13 +76,15 @@ import { ECConnectionState } from "../livekit/useECConnectionState";
|
||||
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||
import { useCallViewModel } from "../state/CallViewModel";
|
||||
import { subscribe } from "../state/subscribe";
|
||||
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
export interface ActiveCallProps
|
||||
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
|
||||
e2eeConfig: E2EEConfig;
|
||||
e2eeSystem: EncryptionSystem;
|
||||
}
|
||||
|
||||
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
@ -91,7 +93,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
props.rtcSession,
|
||||
props.muteStates,
|
||||
sfuConfig,
|
||||
props.e2eeConfig,
|
||||
props.e2eeSystem,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -238,7 +240,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
const vm = useCallViewModel(
|
||||
rtcSession.room,
|
||||
livekitRoom,
|
||||
matrixInfo.roomEncrypted,
|
||||
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
|
||||
connState,
|
||||
);
|
||||
const items = useStateObservable(vm.tiles);
|
||||
@ -432,7 +434,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
id={matrixInfo.roomId}
|
||||
name={matrixInfo.roomName}
|
||||
avatarUrl={matrixInfo.roomAvatar}
|
||||
encrypted={matrixInfo.roomEncrypted}
|
||||
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
</LeftNav>
|
||||
|
@ -25,8 +25,8 @@ import useClipboard from "react-use-clipboard";
|
||||
import { Modal } from "../Modal";
|
||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
import { Toast } from "../Toast";
|
||||
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
@ -36,11 +36,11 @@ interface Props {
|
||||
|
||||
export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const roomSharedKey = useRoomSharedKey(room.roomId);
|
||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||
|
||||
const url = useMemo(
|
||||
() =>
|
||||
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
|
||||
[room, roomSharedKey],
|
||||
() => getAbsoluteRoomUrl(room.roomId, e2eeSystem, room.name),
|
||||
[e2eeSystem, room.name, room.roomId],
|
||||
);
|
||||
const [, setCopied] = useClipboard(url);
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
|
@ -25,6 +25,18 @@ limitations under the License.
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wait {
|
||||
color: var(--cpd-color-text-primary) !important;
|
||||
background-color: var(--cpd-color-bg-canvas-default) !important;
|
||||
/* relative colors are only supported on chromium based browsers */
|
||||
background-color: rgb(
|
||||
from var(--cpd-color-bg-canvas-default) r g b / 0.5
|
||||
) !important;
|
||||
}
|
||||
.wait > svg {
|
||||
color: var(--cpd-color-theme-primary) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.join {
|
||||
width: 100%;
|
||||
|
@ -21,8 +21,8 @@ import { Button, Link } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import styles from "./LobbyView.module.css";
|
||||
import inCallStyles from "./InCallView.module.css";
|
||||
import styles from "./LobbyView.module.css";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||
@ -36,16 +36,19 @@ import {
|
||||
} from "../button/Button";
|
||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||
import { useMediaQuery } from "../useMediaQuery";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
matrixInfo: MatrixInfo;
|
||||
muteStates: MuteStates;
|
||||
onEnter: () => void;
|
||||
enterLabel?: JSX.Element | string;
|
||||
confineToRoom: boolean;
|
||||
hideHeader: boolean;
|
||||
participantCount: number;
|
||||
participantCount: number | null;
|
||||
onShareClick: (() => void) | null;
|
||||
waitingForInvite?: boolean;
|
||||
}
|
||||
|
||||
export const LobbyView: FC<Props> = ({
|
||||
@ -53,10 +56,12 @@ export const LobbyView: FC<Props> = ({
|
||||
matrixInfo,
|
||||
muteStates,
|
||||
onEnter,
|
||||
enterLabel,
|
||||
confineToRoom,
|
||||
hideHeader,
|
||||
participantCount,
|
||||
onShareClick,
|
||||
waitingForInvite,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
useLocationNavigation();
|
||||
@ -104,7 +109,7 @@ export const LobbyView: FC<Props> = ({
|
||||
id={matrixInfo.roomId}
|
||||
name={matrixInfo.roomName}
|
||||
avatarUrl={matrixInfo.roomAvatar}
|
||||
encrypted={matrixInfo.roomEncrypted}
|
||||
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
</LeftNav>
|
||||
@ -116,12 +121,16 @@ export const LobbyView: FC<Props> = ({
|
||||
<div className={styles.content}>
|
||||
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates}>
|
||||
<Button
|
||||
className={styles.join}
|
||||
size="lg"
|
||||
onClick={onEnter}
|
||||
className={classNames(styles.join, {
|
||||
[styles.wait]: waitingForInvite,
|
||||
})}
|
||||
size={waitingForInvite ? "sm" : "lg"}
|
||||
onClick={() => {
|
||||
if (!waitingForInvite) onEnter();
|
||||
}}
|
||||
data-testid="lobby_joinCall"
|
||||
>
|
||||
{t("lobby.join_button")}
|
||||
{enterLabel ?? t("lobby.join_button")}
|
||||
</Button>
|
||||
</VideoPreview>
|
||||
{!recentsButtonInFooter && recentsButton}
|
||||
|
@ -20,10 +20,10 @@ import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
|
||||
/**
|
||||
* If there already is this many participants in the call, we automatically mute
|
||||
* the user
|
||||
* If there already are this many participants in the call, we automatically mute
|
||||
* the user.
|
||||
*/
|
||||
const MUTE_PARTICIPANT_COUNT = 8;
|
||||
export const MUTE_PARTICIPANT_COUNT = 8;
|
||||
|
||||
interface DeviceAvailable {
|
||||
enabled: boolean;
|
||||
@ -51,26 +51,27 @@ function useMuteState(
|
||||
device: MediaDevice,
|
||||
enabledByDefault: () => boolean,
|
||||
): MuteState {
|
||||
const [enabled, setEnabled] = useReactiveState<boolean>(
|
||||
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
|
||||
const [enabled, setEnabled] = useReactiveState<boolean | undefined>(
|
||||
(prev) =>
|
||||
device.available.length > 0 ? prev ?? enabledByDefault() : undefined,
|
||||
[device],
|
||||
);
|
||||
return useMemo(
|
||||
() =>
|
||||
device.available.length === 0
|
||||
? deviceUnavailable
|
||||
: { enabled, setEnabled },
|
||||
: {
|
||||
enabled: enabled ?? false,
|
||||
setEnabled: setEnabled as Dispatch<SetStateAction<boolean>>,
|
||||
},
|
||||
[device, enabled, setEnabled],
|
||||
);
|
||||
}
|
||||
|
||||
export function useMuteStates(participantCount: number): MuteStates {
|
||||
export function useMuteStates(): MuteStates {
|
||||
const devices = useMediaDevices();
|
||||
|
||||
const audio = useMuteState(
|
||||
devices.audioInput,
|
||||
() => participantCount <= MUTE_PARTICIPANT_COUNT,
|
||||
);
|
||||
const audio = useMuteState(devices.audioInput, () => true);
|
||||
const video = useMuteState(devices.videoInput, () => true);
|
||||
|
||||
return useMemo(() => ({ audio, video }), [audio, video]);
|
||||
|
@ -15,8 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CheckIcon from "@vector-im/compound-design-tokens/icons/check.svg?react";
|
||||
|
||||
import { useClientLegacy } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
@ -30,6 +31,11 @@ import { HomePage } from "../home/HomePage";
|
||||
import { platform } from "../Platform";
|
||||
import { AppSelectionModal } from "./AppSelectionModal";
|
||||
import { widget } from "../widget";
|
||||
import { GroupCallStatus } from "./useLoadGroupCall";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import { useMuteStates } from "./MuteStates";
|
||||
|
||||
export const RoomPage: FC = () => {
|
||||
const {
|
||||
@ -40,7 +46,7 @@ export const RoomPage: FC = () => {
|
||||
displayName,
|
||||
skipLobby,
|
||||
} = useUrlParams();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { roomAlias, roomId, viaServers } = useRoomIdentifier();
|
||||
|
||||
const roomIdOrAlias = roomId ?? roomAlias;
|
||||
@ -48,17 +54,14 @@ export const RoomPage: FC = () => {
|
||||
logger.error("No room specified");
|
||||
}
|
||||
|
||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// During the beta, opt into analytics by default
|
||||
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
|
||||
}, [optInAnalytics, setOptInAnalytics]);
|
||||
|
||||
const { loading, authenticated, client, error, passwordlessUser } =
|
||||
useClientLegacy();
|
||||
const { avatarUrl, displayName: userDisplayName } = useProfile(client);
|
||||
|
||||
const muteStates = useMuteStates();
|
||||
|
||||
useEffect(() => {
|
||||
// If we've finished loading, are not already authed and we've been given a display name as
|
||||
@ -77,19 +80,87 @@ export const RoomPage: FC = () => {
|
||||
registerPasswordlessUser,
|
||||
]);
|
||||
|
||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||
useEffect(() => {
|
||||
// During the beta, opt into analytics by default
|
||||
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
|
||||
}, [optInAnalytics, setOptInAnalytics]);
|
||||
|
||||
const groupCallView = useCallback(
|
||||
(rtcSession: MatrixRTCSession) => (
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
rtcSession={rtcSession}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
),
|
||||
[client, passwordlessUser, confineToRoom, preload, hideHeader, skipLobby],
|
||||
(groupCallState: GroupCallStatus): JSX.Element => {
|
||||
switch (groupCallState.kind) {
|
||||
case "loaded":
|
||||
return (
|
||||
<GroupCallView
|
||||
client={client!}
|
||||
rtcSession={groupCallState.rtcSession}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
confineToRoom={confineToRoom}
|
||||
preload={preload}
|
||||
skipLobby={skipLobby}
|
||||
hideHeader={hideHeader}
|
||||
muteStates={muteStates}
|
||||
/>
|
||||
);
|
||||
case "waitForInvite":
|
||||
case "canKnock": {
|
||||
const knock =
|
||||
groupCallState.kind === "canKnock" ? groupCallState.knock : null;
|
||||
const label: string | JSX.Element =
|
||||
groupCallState.kind === "canKnock" ? (
|
||||
t("lobby.ask_to_join")
|
||||
) : (
|
||||
<>
|
||||
{t("lobby.waiting_for_invite")}
|
||||
<CheckIcon />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<LobbyView
|
||||
client={client!}
|
||||
matrixInfo={{
|
||||
userId: client!.getUserId() ?? "",
|
||||
displayName: userDisplayName ?? "",
|
||||
avatarUrl: avatarUrl ?? "",
|
||||
roomAlias: null,
|
||||
roomId: groupCallState.roomSummary.room_id,
|
||||
roomName: groupCallState.roomSummary.name ?? "",
|
||||
roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
|
||||
e2eeSystem: {
|
||||
kind: groupCallState.roomSummary[
|
||||
"im.nheko.summary.encryption"
|
||||
]
|
||||
? E2eeType.PER_PARTICIPANT
|
||||
: E2eeType.NONE,
|
||||
},
|
||||
}}
|
||||
onEnter={(): void => knock?.()}
|
||||
enterLabel={label}
|
||||
waitingForInvite={groupCallState.kind === "waitForInvite"}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={hideHeader}
|
||||
participantCount={null}
|
||||
muteStates={muteStates}
|
||||
onShareClick={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return <> </>;
|
||||
}
|
||||
},
|
||||
[
|
||||
client,
|
||||
passwordlessUser,
|
||||
confineToRoom,
|
||||
preload,
|
||||
skipLobby,
|
||||
hideHeader,
|
||||
muteStates,
|
||||
t,
|
||||
userDisplayName,
|
||||
avatarUrl,
|
||||
],
|
||||
);
|
||||
|
||||
let content: ReactNode;
|
||||
@ -118,9 +189,9 @@ export const RoomPage: FC = () => {
|
||||
<>
|
||||
{content}
|
||||
{/* On Android and iOS, show a prompt to launch the mobile app. */}
|
||||
{appPrompt && (platform === "android" || platform === "ios") && (
|
||||
<AppSelectionModal roomId={roomId} />
|
||||
)}
|
||||
{appPrompt &&
|
||||
(platform === "android" || platform === "ios") &&
|
||||
roomId && <AppSelectionModal roomId={roomId} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, FC, ReactNode } from "react";
|
||||
import { useEffect, useMemo, useRef, FC, ReactNode, useCallback } from "react";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
import { usePreviewTracks } from "@livekit/components-react";
|
||||
@ -32,6 +32,7 @@ import styles from "./VideoPreview.module.css";
|
||||
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||
import { MuteStates } from "./MuteStates";
|
||||
import { useMediaQuery } from "../useMediaQuery";
|
||||
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
export type MatrixInfo = {
|
||||
userId: string;
|
||||
@ -41,7 +42,7 @@ export type MatrixInfo = {
|
||||
roomName: string;
|
||||
roomAlias: string | null;
|
||||
roomAvatar: string | null;
|
||||
roomEncrypted: boolean;
|
||||
e2eeSystem: EncryptionSystem;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@ -67,8 +68,8 @@ export const VideoPreview: FC<Props> = ({
|
||||
deviceId: devices.audioInput.selectedId,
|
||||
};
|
||||
|
||||
const tracks = usePreviewTracks(
|
||||
{
|
||||
const localTrackOptions = useMemo(
|
||||
() => ({
|
||||
// The only reason we request audio here is to get the audio permission
|
||||
// request over with at the same time. But changing the audio settings
|
||||
// shouldn't cause this hook to recreate the track, which is why we
|
||||
@ -79,13 +80,21 @@ export const VideoPreview: FC<Props> = ({
|
||||
video: muteStates.video.enabled && {
|
||||
deviceId: devices.videoInput.selectedId,
|
||||
},
|
||||
},
|
||||
(error) => {
|
||||
}),
|
||||
[devices.videoInput.selectedId, muteStates.video.enabled],
|
||||
);
|
||||
|
||||
const onError = useCallback(
|
||||
(error: Error) => {
|
||||
logger.error("Error while creating preview Tracks:", error);
|
||||
muteStates.audio.setEnabled?.(false);
|
||||
muteStates.video.setEnabled?.(false);
|
||||
},
|
||||
[muteStates.audio, muteStates.video],
|
||||
);
|
||||
|
||||
const tracks = usePreviewTracks(localTrackOptions, onError);
|
||||
|
||||
const videoTrack = useMemo(
|
||||
() =>
|
||||
tracks?.find((t) => t.kind === Track.Kind.Video) as
|
||||
|
@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {
|
||||
ClientEvent,
|
||||
MatrixClient,
|
||||
RoomSummary,
|
||||
} from "matrix-js-sdk/src/client";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { RoomEvent, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { JoinRule } from "matrix-js-sdk";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { widget } from "../widget";
|
||||
|
||||
export type GroupCallLoaded = {
|
||||
kind: "loaded";
|
||||
@ -38,14 +45,48 @@ export type GroupCallLoading = {
|
||||
kind: "loading";
|
||||
};
|
||||
|
||||
export type GroupCallWaitForInvite = {
|
||||
kind: "waitForInvite";
|
||||
roomSummary: RoomSummary;
|
||||
};
|
||||
|
||||
export type GroupCallCanKnock = {
|
||||
kind: "canKnock";
|
||||
roomSummary: RoomSummary;
|
||||
knock: () => void;
|
||||
};
|
||||
|
||||
export type GroupCallStatus =
|
||||
| GroupCallLoaded
|
||||
| GroupCallLoadFailed
|
||||
| GroupCallLoading;
|
||||
| GroupCallLoading
|
||||
| GroupCallWaitForInvite
|
||||
| GroupCallCanKnock;
|
||||
|
||||
export interface GroupCallLoadState {
|
||||
error?: Error;
|
||||
groupCall?: GroupCall;
|
||||
export class CallTerminatedMessage extends Error {
|
||||
/**
|
||||
* @param messageBody The message explaining the kind of termination (kick, ban, knock reject, etc.) (translated)
|
||||
*/
|
||||
public messageBody: string;
|
||||
/**
|
||||
* @param reason The user provided reason for the termination (kick/ban)
|
||||
*/
|
||||
public reason?: string;
|
||||
/**
|
||||
*
|
||||
* @param messageTitle The title of the call ended screen message (translated)
|
||||
* @param messageBody The message explaining the kind of termination (kick, ban, knock reject, etc.) (translated)
|
||||
* @param reason The user provided reason for the termination (kick/ban)
|
||||
*/
|
||||
public constructor(
|
||||
messageTitle: string,
|
||||
messageBody: string,
|
||||
reason?: string,
|
||||
) {
|
||||
super(messageTitle);
|
||||
this.messageBody = messageBody;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
export const useLoadGroupCall = (
|
||||
@ -53,36 +94,159 @@ export const useLoadGroupCall = (
|
||||
roomIdOrAlias: string,
|
||||
viaServers: string[],
|
||||
): GroupCallStatus => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
const activeRoom = useRef<Room>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const bannedError = useCallback(
|
||||
(): CallTerminatedMessage =>
|
||||
new CallTerminatedMessage(
|
||||
t("group_call_loader.banned_heading"),
|
||||
t("group_call_loader.banned_body"),
|
||||
leaveReason(),
|
||||
),
|
||||
[t],
|
||||
);
|
||||
const knockRejectError = useCallback(
|
||||
(): CallTerminatedMessage =>
|
||||
new CallTerminatedMessage(
|
||||
t("group_call_loader.knock_reject_heading"),
|
||||
t("group_call_loader.knock_reject_body"),
|
||||
leaveReason(),
|
||||
),
|
||||
[t],
|
||||
);
|
||||
const removeNoticeError = useCallback(
|
||||
(): CallTerminatedMessage =>
|
||||
new CallTerminatedMessage(
|
||||
t("group_call_loader.call_ended_heading"),
|
||||
t("group_call_loader.call_ended_body"),
|
||||
leaveReason(),
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const leaveReason = (): string =>
|
||||
activeRoom.current?.currentState
|
||||
.getStateEvents(EventType.RoomMember, activeRoom.current?.myUserId)
|
||||
?.getContent().reason;
|
||||
|
||||
useEffect(() => {
|
||||
const getRoomByAlias = async (alias: string): Promise<Room> => {
|
||||
// We lowercase the localpart when we create the room, so we must lowercase
|
||||
// it here too (we just do the whole alias). We can't do the same to room IDs
|
||||
// though.
|
||||
// Also, we explicitly look up the room alias here. We previously just tried to
|
||||
// join anyway but the js-sdk recreates the room if you pass the alias for a
|
||||
// room you're already joined to (which it probably ought not to).
|
||||
let room: Room | null = null;
|
||||
const lookupResult = await client.getRoomIdForAlias(alias.toLowerCase());
|
||||
logger.info(`${alias} resolved to ${lookupResult.room_id}`);
|
||||
room = client.getRoom(lookupResult.room_id);
|
||||
if (!room) {
|
||||
logger.info(`Room ${lookupResult.room_id} not found, joining.`);
|
||||
room = await client.joinRoom(lookupResult.room_id, {
|
||||
viaServers: lookupResult.servers,
|
||||
});
|
||||
} else {
|
||||
logger.info(`Already in room ${lookupResult.room_id}, not rejoining.`);
|
||||
}
|
||||
return room;
|
||||
};
|
||||
|
||||
const getRoomByKnocking = async (
|
||||
roomId: string,
|
||||
viaServers: string[],
|
||||
onKnockSent: () => void,
|
||||
): Promise<Room> => {
|
||||
let joinedRoom: Room | null = null;
|
||||
await client.knockRoom(roomId, { viaServers });
|
||||
onKnockSent();
|
||||
const invitePromise = new Promise<void>((resolve, reject) => {
|
||||
client.on(
|
||||
RoomEvent.MyMembership,
|
||||
async (room, membership, prevMembership) => {
|
||||
if (roomId !== room.roomId) return;
|
||||
activeRoom.current = room;
|
||||
if (membership === KnownMembership.Invite) {
|
||||
await client.joinRoom(room.roomId, { viaServers });
|
||||
joinedRoom = room;
|
||||
logger.log("Auto-joined %s", room.roomId);
|
||||
resolve();
|
||||
}
|
||||
if (membership === KnownMembership.Ban) reject(bannedError());
|
||||
if (membership === KnownMembership.Leave)
|
||||
reject(knockRejectError());
|
||||
},
|
||||
);
|
||||
});
|
||||
await invitePromise;
|
||||
if (!joinedRoom) {
|
||||
throw new Error("Failed to join room after knocking.");
|
||||
}
|
||||
return joinedRoom;
|
||||
};
|
||||
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
let room: Room | null = null;
|
||||
if (roomIdOrAlias[0] === "#") {
|
||||
// We lowercase the localpart when we create the room, so we must lowercase
|
||||
// it here too (we just do the whole alias). We can't do the same to room IDs
|
||||
// though.
|
||||
// Also, we explicitly look up the room alias here. We previously just tried to
|
||||
// join anyway but the js-sdk recreates the room if you pass the alias for a
|
||||
// room you're already joined to (which it probably ought not to).
|
||||
const lookupResult = await client.getRoomIdForAlias(
|
||||
roomIdOrAlias.toLowerCase(),
|
||||
);
|
||||
logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`);
|
||||
room = client.getRoom(lookupResult.room_id);
|
||||
if (!room) {
|
||||
logger.info(`Room ${lookupResult.room_id} not found, joining.`);
|
||||
room = await client.joinRoom(lookupResult.room_id, {
|
||||
viaServers: lookupResult.servers,
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`Already in room ${lookupResult.room_id}, not rejoining.`,
|
||||
);
|
||||
}
|
||||
const alias = roomIdOrAlias;
|
||||
// The call uses a room alias
|
||||
room = await getRoomByAlias(alias);
|
||||
activeRoom.current = room;
|
||||
} else {
|
||||
room = await client.joinRoom(roomIdOrAlias, { viaServers });
|
||||
// The call uses a room_id
|
||||
const roomId = roomIdOrAlias;
|
||||
|
||||
// first try if the room already exists
|
||||
// - in widget mode
|
||||
// - in SPA mode if the user already joined the room
|
||||
room = client.getRoom(roomId);
|
||||
activeRoom.current = room ?? undefined;
|
||||
if (room?.getMyMembership() === KnownMembership.Join) {
|
||||
// room already joined so we are done here already.
|
||||
return room!;
|
||||
}
|
||||
if (widget)
|
||||
// in widget mode we never should reach this point. (getRoom should return the room.)
|
||||
throw new Error(
|
||||
"Room not found. The widget-api did not pass over the relevant room events/information.",
|
||||
);
|
||||
|
||||
// If the room does not exist we first search for it with viaServers
|
||||
const roomSummary = await client.getRoomSummary(roomId, viaServers);
|
||||
if (room?.getMyMembership() === KnownMembership.Ban) {
|
||||
throw bannedError();
|
||||
} else {
|
||||
if (roomSummary.join_rule === JoinRule.Public) {
|
||||
room = await client.joinRoom(roomSummary.room_id, {
|
||||
viaServers,
|
||||
});
|
||||
} else if (roomSummary.join_rule === JoinRule.Knock) {
|
||||
let knock: () => void = () => {};
|
||||
const userPressedAskToJoinPromise: Promise<void> = new Promise(
|
||||
(resolve) => {
|
||||
if (roomSummary.membership !== KnownMembership.Knock) {
|
||||
knock = resolve;
|
||||
} else {
|
||||
// resolve immediately if the user already knocked
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
setState({ kind: "canKnock", roomSummary, knock });
|
||||
await userPressedAskToJoinPromise;
|
||||
room = await getRoomByKnocking(
|
||||
roomSummary.room_id,
|
||||
viaServers,
|
||||
() => setState({ kind: "waitForInvite", roomSummary }),
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Room ${roomSummary.room_id} is not joinable. This likely means, that the conference owner has changed the room settings to private.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@ -95,6 +259,7 @@ export const useLoadGroupCall = (
|
||||
|
||||
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
||||
const room = await fetchOrCreateRoom();
|
||||
activeRoom.current = room;
|
||||
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
||||
|
||||
const rtcSession = client.matrixRTC.getRoomSession(room);
|
||||
@ -119,11 +284,33 @@ export const useLoadGroupCall = (
|
||||
}
|
||||
};
|
||||
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}, [client, roomIdOrAlias, viaServers, t]);
|
||||
const observeMyMembership = async (): Promise<void> => {
|
||||
await new Promise((_, reject) => {
|
||||
client.on(RoomEvent.MyMembership, async (_, membership) => {
|
||||
if (membership === KnownMembership.Leave) reject(removeNoticeError());
|
||||
if (membership === KnownMembership.Ban) reject(bannedError());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (state.kind === "loading") {
|
||||
logger.log("Start loading group call");
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||
.then(observeMyMembership)
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}
|
||||
}, [
|
||||
bannedError,
|
||||
client,
|
||||
knockRejectError,
|
||||
removeNoticeError,
|
||||
roomIdOrAlias,
|
||||
state,
|
||||
t,
|
||||
viaServers,
|
||||
]);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
@ -298,13 +298,13 @@ export function useRageshakeRequest(): (
|
||||
|
||||
const sendRageshakeRequest = useCallback(
|
||||
(roomId: string, rageshakeRequestId: string) => {
|
||||
// @ts-expect-error - org.matrix.rageshake_request is not part of `keyof TimelineEvents` but it is okay to sent a custom event.
|
||||
client!.sendEvent(roomId, "org.matrix.rageshake_request", {
|
||||
request_id: rageshakeRequestId,
|
||||
});
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
return sendRageshakeRequest;
|
||||
}
|
||||
|
||||
|
@ -122,6 +122,7 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
];
|
||||
const receiveState = [
|
||||
{ eventType: EventType.RoomMember },
|
||||
{ eventType: EventType.RoomEncryption },
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix },
|
||||
];
|
||||
|
56
yarn.lock
56
yarn.lock
@ -1120,9 +1120,9 @@
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@bufbuild/protobuf@^1.7.2":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.8.0.tgz#1c8651ea34adb8019b483e09de02aeeb1cd57d79"
|
||||
integrity sha512-qR9FwI8QKIveDnUYutvfzbC21UZJJryYrLuZGjeZ/VGz+vXelUkK+xgkOHsvPEdYEdxtgUUq4313N8QtOehJ1Q==
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.9.0.tgz#fffac3183059a41ceef5311e07e3724d426a95c4"
|
||||
integrity sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ==
|
||||
|
||||
"@csstools/cascade-layer-name-parser@^1.0.8":
|
||||
version "1.0.8"
|
||||
@ -1718,17 +1718,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||
|
||||
"@livekit/components-core@0.9.3":
|
||||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@livekit/components-core/-/components-core-0.9.3.tgz#327980a784a9e62cff100915f6f952714f499860"
|
||||
integrity sha512-RUhKw/eg2frnOHq6Xurfg4HqawmdpC/o8Dkp+J6PgnieA6mSQOOez7mUdPNqsAybnLujjJvVJ735sZJRqTb1Sg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.6.3"
|
||||
email-regex "5.0.0"
|
||||
loglevel "1.9.1"
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@livekit/components-core@^0.10.0":
|
||||
"@livekit/components-core@0.10.0", "@livekit/components-core@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@livekit/components-core/-/components-core-0.10.0.tgz#dfd1ccf72518d89bba2d9d10fadbbf2dd47ed321"
|
||||
integrity sha512-TSsIG2BRLABT5FP+5sueZgkByGYyFhv3UTb8fneWchvQRBHtiU9s4FF8SIoAw9z3znhwp1tKaJyuIyKp7k0Juw==
|
||||
@ -1739,11 +1729,11 @@
|
||||
rxjs "7.8.1"
|
||||
|
||||
"@livekit/components-react@^2.0.0":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.0.6.tgz#c4ff790180f86f6a3cfc341fe5c0b93bac8bf92b"
|
||||
integrity sha512-L0iaIPasPJLftI6FBcWcGicmGpw5LKKFvMV6GSyuv0q8oYrmUYoLLQdGmw7eW4q+7bQFAcFHeD9BzMXdprzCgw==
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.2.0.tgz#a2c9a1568055cf07d4784b98336f28d574638bf8"
|
||||
integrity sha512-TDa2YNBphkdf2dz85pEZs1UBl8wD/LHFeYupNoTqjtlLVlTXpr09Buv3/eegQFJhXoDSK6fAYqKZ4U/oYydv/w==
|
||||
dependencies:
|
||||
"@livekit/components-core" "0.9.3"
|
||||
"@livekit/components-core" "0.10.0"
|
||||
"@react-hook/latest" "1.0.3"
|
||||
clsx "2.1.0"
|
||||
usehooks-ts "2.16.0"
|
||||
@ -1755,10 +1745,10 @@
|
||||
dependencies:
|
||||
"@bufbuild/protobuf" "^1.7.2"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.6.0.tgz#35224214c7638abbe2bc91fb4fa4fb022a1a2bf0"
|
||||
integrity sha512-v9PFWzSTWMlZKbyk3PPsZjUtOEQ7FIz5USD3lFRUWiS4pv0FOKR125VOUnR5Z/kAty57JXCHDAexCln3zE2Fww==
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.9.0":
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.9.0.tgz#9dfed83e33f760650596c4e5c520e5e4c53355d2"
|
||||
integrity sha512-/bgA4QfE7qkK6GFr9hnhjAvRSebGrmEJxukU0ukbudZcYvbzymoBBM8j3HeULXZT8kbw8WH6z63txYTMCBSDOA==
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||
version "3.2.14"
|
||||
@ -3289,9 +3279,9 @@
|
||||
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
||||
|
||||
"@types/node@*", "@types/node@^20.0.0":
|
||||
version "20.12.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.6.tgz#72d068870518d7da1d97b49db401e2d6a1805294"
|
||||
integrity sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==
|
||||
version "20.12.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384"
|
||||
integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@ -6164,9 +6154,9 @@ lines-and-columns@^1.1.6:
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
livekit-client@^2.0.2:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.1.0.tgz#3c4f2754eb38933a6d232fe35717bfe3a378bdcf"
|
||||
integrity sha512-nJwfRKw1Pafd2napk66l30dlBjsv1VZ+na3mzNezcAFAYT2lQ4Gch57TdbMBDYo+QfrZ98s+kuZzsFhBwM5rqw==
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.1.1.tgz#c3e1cc2f11727b7a760c801ba98a97585dda031f"
|
||||
integrity sha512-ffnXHQt210GPJ9sR846o7g0lCg/3TJqZxdu55mzQFS1YXGgn9PYKGzcAhKtuOsQ0NEkkn1zKQ0ABHBt7iADiqg==
|
||||
dependencies:
|
||||
"@livekit/protocol" "1.13.0"
|
||||
events "^3.3.0"
|
||||
@ -6308,13 +6298,13 @@ matrix-events-sdk@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#d55c6a36df539f6adacc335efe5b9be27c9cee4a":
|
||||
version "31.4.0"
|
||||
uid d55c6a36df539f6adacc335efe5b9be27c9cee4a
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d55c6a36df539f6adacc335efe5b9be27c9cee4a"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e874468ba3e84819cf4b342d2e66af67ab4cf804":
|
||||
version "32.0.0"
|
||||
uid e874468ba3e84819cf4b342d2e66af67ab4cf804
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e874468ba3e84819cf4b342d2e66af67ab4cf804"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm" "^4.6.0"
|
||||
"@matrix-org/matrix-sdk-crypto-wasm" "^4.9.0"
|
||||
another-json "^0.2.0"
|
||||
bs58 "^5.0.0"
|
||||
content-type "^1.0.4"
|
||||
|
Loading…
Reference in New Issue
Block a user