Merge branch 'livekit' into layout-state
This commit is contained in:
4
.github/workflows/docker.yaml
vendored
4
.github/workflows/docker.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
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
|
name: build-output
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@a530e948adbeb357dbca95a7f8845d385edf4438
|
uses: docker/setup-buildx-action@a530e948adbeb357dbca95a7f8845d385edf4438
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@7e6f77677b7892794c8852c6e3773c3e9bc3129a
|
uses: docker/build-push-action@eb539f44b153603ccbfbd98e2ab9d4d0dcaf23a4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
9
.github/workflows/publish.yaml
vendored
9
.github/workflows/publish.yaml
vendored
@@ -15,7 +15,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_element_call:
|
build_element_call:
|
||||||
if: ${{ github.event.workflow_run.event == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
uses: ./.github/workflows/element-call.yaml
|
uses: ./.github/workflows/element-call.yaml
|
||||||
with:
|
with:
|
||||||
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
vite_app_version: ${{ github.event.release.tag_name || github.sha }}
|
||||||
@@ -25,6 +25,8 @@ jobs:
|
|||||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
publish_tarball:
|
publish_tarball:
|
||||||
|
needs: build_element_call
|
||||||
|
if: always()
|
||||||
name: Publish tarball
|
name: Publish tarball
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
@@ -40,7 +42,7 @@ jobs:
|
|||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
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
|
name: build-output
|
||||||
path: dist
|
path: dist
|
||||||
- name: Create Tarball
|
- name: Create Tarball
|
||||||
@@ -49,13 +51,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@b06cde36fc32a3ee94080e87258567f73f921537
|
uses: actions/upload-artifact@552bf3722c16e81001aea7db72d8cedf64eb5f68
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
with:
|
with:
|
||||||
path: "./element-call-*.tar.gz"
|
path: "./element-call-*.tar.gz"
|
||||||
publish_docker:
|
publish_docker:
|
||||||
needs: publish_tarball
|
needs: publish_tarball
|
||||||
|
if: always()
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: 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
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@v6.0.3
|
uses: peter-evans/create-pull-request@v6.0.5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
branch: actions/localazy-download
|
branch: actions/localazy-download
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
port: 7880
|
port: 7880
|
||||||
environment: dev
|
|
||||||
bind_addresses:
|
bind_addresses:
|
||||||
- "0.0.0.0"
|
- "0.0.0.0"
|
||||||
rtc:
|
rtc:
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"i18next-http-backend": "^2.0.0",
|
"i18next-http-backend": "^2.0.0",
|
||||||
"livekit-client": "^2.0.2",
|
"livekit-client": "^2.0.2",
|
||||||
"lodash": "^4.17.21",
|
"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",
|
"matrix-widget-api": "^1.3.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
|
|||||||
@@ -60,8 +60,17 @@
|
|||||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
"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_description": "<0>Submitting debug logs will help us track down the problem.</0>",
|
||||||
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
|
"full_screen_view_h1": "<0>Oops, something's gone wrong.</0>",
|
||||||
"group_call_loader_failed_heading": "Call not found",
|
"group_call_loader": {
|
||||||
"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.",
|
"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",
|
"hangup_button_label": "End call",
|
||||||
"header_label": "Element Call Home",
|
"header_label": "Element Call Home",
|
||||||
"header_participants_label": "Participants",
|
"header_participants_label": "Participants",
|
||||||
@@ -77,8 +86,10 @@
|
|||||||
"layout_grid_label": "Grid",
|
"layout_grid_label": "Grid",
|
||||||
"layout_spotlight_label": "Spotlight",
|
"layout_spotlight_label": "Spotlight",
|
||||||
"lobby": {
|
"lobby": {
|
||||||
|
"ask_to_join": "Ask to join call",
|
||||||
"join_button": "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",
|
"log_in": "Log In",
|
||||||
"logging_in": "Logging in…",
|
"logging_in": "Logging in…",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ interface ErrorViewProps {
|
|||||||
|
|
||||||
export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { confineToRoom } = useUrlParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,25 +79,26 @@ export const ErrorView: FC<ErrorViewProps> = ({ error }) => {
|
|||||||
: error.message}
|
: error.message}
|
||||||
</p>
|
</p>
|
||||||
<RageshakeButton description={`***Error View***: ${error.message}`} />
|
<RageshakeButton description={`***Error View***: ${error.message}`} />
|
||||||
{location.pathname === "/" ? (
|
{!confineToRoom &&
|
||||||
<Button
|
(location.pathname === "/" ? (
|
||||||
size="lg"
|
<Button
|
||||||
variant="default"
|
size="lg"
|
||||||
className={styles.homeLink}
|
variant="default"
|
||||||
onPress={onReload}
|
className={styles.homeLink}
|
||||||
>
|
onPress={onReload}
|
||||||
{t("return_home_button")}
|
>
|
||||||
</Button>
|
{t("return_home_button")}
|
||||||
) : (
|
</Button>
|
||||||
<LinkButton
|
) : (
|
||||||
size="lg"
|
<LinkButton
|
||||||
variant="default"
|
size="lg"
|
||||||
className={styles.homeLink}
|
variant="default"
|
||||||
to="/"
|
className={styles.homeLink}
|
||||||
>
|
to="/"
|
||||||
{t("return_home_button")}
|
>
|
||||||
</LinkButton>
|
{t("return_home_button")}
|
||||||
)}
|
</LinkButton>
|
||||||
|
))}
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ interface RoomHeaderInfoProps {
|
|||||||
name: string;
|
name: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
encrypted: boolean;
|
encrypted: boolean;
|
||||||
participantCount: number;
|
participantCount: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
||||||
@@ -150,7 +150,7 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
|||||||
</Heading>
|
</Heading>
|
||||||
<EncryptionLock encrypted={encrypted} />
|
<EncryptionLock encrypted={encrypted} />
|
||||||
</div>
|
</div>
|
||||||
{participantCount > 0 && (
|
{(participantCount ?? 0) > 0 && (
|
||||||
<div className={styles.participantsLine}>
|
<div className={styles.participantsLine}>
|
||||||
<UserProfileIcon
|
<UserProfileIcon
|
||||||
width={20}
|
width={20}
|
||||||
@@ -158,7 +158,7 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
|
|||||||
aria-label={t("header_participants_label")}
|
aria-label={t("header_participants_label")}
|
||||||
/>
|
/>
|
||||||
<Text as="span" size="sm" weight="medium">
|
<Text as="span" size="sm" weight="medium">
|
||||||
{t("participant_count", { count: participantCount })}
|
{t("participant_count", { count: participantCount ?? 0 })}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ limitations under the License.
|
|||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
|
import { EncryptionSystem } from "./e2ee/sharedKeyManagement";
|
||||||
export const PASSWORD_STRING = "password=";
|
import { E2eeType } from "./e2ee/e2eeType";
|
||||||
|
|
||||||
interface RoomIdentifier {
|
interface RoomIdentifier {
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
@@ -328,3 +329,32 @@ export const useRoomIdentifier = (): RoomIdentifier => {
|
|||||||
[pathname, search, hash],
|
[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 { useEffect, useMemo } from "react";
|
||||||
import { Room } from "matrix-js-sdk";
|
|
||||||
|
|
||||||
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
|
import { setLocalStorageItem, useLocalStorage } from "../useLocalStorage";
|
||||||
import { useClient } from "../ClientContext";
|
|
||||||
import { UrlParams, getUrlParams, useUrlParams } from "../UrlParams";
|
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 {
|
export function saveKeyForRoom(roomId: string, password: string): void {
|
||||||
setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password);
|
setLocalStorageItem(getRoomSharedKeyLocalStorageKey(roomId), password);
|
||||||
@@ -68,30 +67,37 @@ const useKeyFromUrl = (): [string, string] | [undefined, undefined] => {
|
|||||||
: [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
|
// make sure we've extracted the key from the URL first
|
||||||
// (and we still need to take the value it returns because
|
// (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
|
// the effect won't run in time for it to save to localstorage in
|
||||||
// time for us to read it out again).
|
// time for us to read it out again).
|
||||||
const [urlRoomId, passwordFormUrl] = useKeyFromUrl();
|
const [urlRoomId, passwordFromUrl] = useKeyFromUrl();
|
||||||
|
|
||||||
const storedPassword = useInternalRoomSharedKey(roomId);
|
const storedPassword = useInternalRoomSharedKey(roomId);
|
||||||
|
const room = client?.getRoom(roomId);
|
||||||
if (storedPassword) return storedPassword;
|
const e2eeSystem = <EncryptionSystem>useMemo(() => {
|
||||||
if (urlRoomId === roomId) return passwordFormUrl;
|
if (!room) return { kind: E2eeType.NONE };
|
||||||
return undefined;
|
if (storedPassword)
|
||||||
};
|
return {
|
||||||
|
kind: E2eeType.SHARED_KEY,
|
||||||
export const useIsRoomE2EE = (roomId: string): boolean | null => {
|
secret: storedPassword,
|
||||||
const { client } = useClient();
|
};
|
||||||
const room = useMemo(() => client?.getRoom(roomId), [roomId, client]);
|
if (urlRoomId === roomId)
|
||||||
|
return {
|
||||||
return useMemo(() => !room || isRoomE2EE(room), [room]);
|
kind: E2eeType.SHARED_KEY,
|
||||||
};
|
secret: passwordFromUrl,
|
||||||
|
};
|
||||||
export function isRoomE2EE(room: Room): boolean {
|
if (room.hasEncryptionStateEvent()) {
|
||||||
// For now, rooms in widget mode are never considered encrypted.
|
return { kind: E2eeType.PER_PARTICIPANT };
|
||||||
// In the future, when widget mode gains encryption support, then perhaps we
|
}
|
||||||
// should inspect the e2eEnabled URL parameter here?
|
return { kind: E2eeType.NONE };
|
||||||
return widget === null && !room.getCanonicalAlias();
|
}, [passwordFromUrl, room, roomId, storedPassword, urlRoomId]);
|
||||||
|
return e2eeSystem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,12 +78,14 @@ export const RegisteredView: FC<Props> = ({ client }) => {
|
|||||||
roomName,
|
roomName,
|
||||||
E2eeType.SHARED_KEY,
|
E2eeType.SHARED_KEY,
|
||||||
);
|
);
|
||||||
|
if (!createRoomResult.password)
|
||||||
|
throw new Error("Failed to create room with shared secret");
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
getRelativeRoomUrl(
|
getRelativeRoomUrl(
|
||||||
createRoomResult.roomId,
|
createRoomResult.roomId,
|
||||||
|
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
|
||||||
roomName,
|
roomName,
|
||||||
createRoomResult.password,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,13 +116,15 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
if (!setClient) {
|
if (!setClient) {
|
||||||
throw new Error("setClient is undefined");
|
throw new Error("setClient is undefined");
|
||||||
}
|
}
|
||||||
|
if (!createRoomResult.password)
|
||||||
|
throw new Error("Failed to create room with shared secret");
|
||||||
|
|
||||||
setClient({ client, session });
|
setClient({ client, session });
|
||||||
history.push(
|
history.push(
|
||||||
getRelativeRoomUrl(
|
getRelativeRoomUrl(
|
||||||
createRoomResult.roomId,
|
createRoomResult.roomId,
|
||||||
|
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
|
||||||
roomName,
|
roomName,
|
||||||
createRoomResult.password,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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[] {
|
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) {
|
// We want to show all rooms that historically had a call and which we are (can become) part of.
|
||||||
return;
|
const rooms = client
|
||||||
}
|
.getRooms()
|
||||||
|
.filter(roomHasCallMembershipEvents)
|
||||||
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 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]);
|
||||||
|
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ import {
|
|||||||
} from "./useECConnectionState";
|
} from "./useECConnectionState";
|
||||||
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
export type E2EEConfig = {
|
|
||||||
mode: E2eeType;
|
|
||||||
sharedKey?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface UseLivekitResult {
|
interface UseLivekitResult {
|
||||||
livekitRoom?: Room;
|
livekitRoom?: Room;
|
||||||
@@ -56,41 +52,35 @@ export function useLiveKit(
|
|||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
sfuConfig: SFUConfig | undefined,
|
sfuConfig: SFUConfig | undefined,
|
||||||
e2eeConfig: E2EEConfig,
|
e2eeSystem: EncryptionSystem,
|
||||||
): UseLivekitResult {
|
): UseLivekitResult {
|
||||||
const e2eeOptions = useMemo((): E2EEOptions | undefined => {
|
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 {
|
return {
|
||||||
keyProvider: new MatrixKeyProvider(),
|
keyProvider: new MatrixKeyProvider(),
|
||||||
worker: new E2EEWorker(),
|
worker: new E2EEWorker(),
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
|
||||||
e2eeConfig.mode === E2eeType.SHARED_KEY &&
|
|
||||||
e2eeConfig.sharedKey
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
keyProvider: new ExternalE2EEKeyProvider(),
|
keyProvider: new ExternalE2EEKeyProvider(),
|
||||||
worker: new E2EEWorker(),
|
worker: new E2EEWorker(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [e2eeConfig]);
|
}, [e2eeSystem]);
|
||||||
|
|
||||||
useEffect(() => {
|
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);
|
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
|
||||||
} else if (
|
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
|
||||||
e2eeConfig.mode === E2eeType.SHARED_KEY &&
|
|
||||||
e2eeConfig.sharedKey
|
|
||||||
) {
|
|
||||||
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
|
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider).setKey(
|
||||||
e2eeConfig.sharedKey,
|
e2eeSystem.secret,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [e2eeOptions, e2eeConfig, rtcSession]);
|
}, [e2eeOptions, e2eeSystem, rtcSession]);
|
||||||
|
|
||||||
const initialMuteStates = useRef<MuteStates>(muteStates);
|
const initialMuteStates = useRef<MuteStates>(muteStates);
|
||||||
const devices = useMediaDevices();
|
const devices = useMediaDevices();
|
||||||
@@ -131,9 +121,9 @@ export function useLiveKit(
|
|||||||
// useEffect() with an argument that references itself, if E2EE is enabled
|
// useEffect() with an argument that references itself, if E2EE is enabled
|
||||||
const room = useMemo(() => {
|
const room = useMemo(() => {
|
||||||
const r = new Room(roomOptions);
|
const r = new Room(roomOptions);
|
||||||
r.setE2EEEnabled(e2eeConfig.mode !== E2eeType.NONE);
|
r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE);
|
||||||
return r;
|
return r;
|
||||||
}, [roomOptions, e2eeConfig]);
|
}, [roomOptions, e2eeSystem]);
|
||||||
|
|
||||||
const connectionState = useECConnectionState(
|
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 { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-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 { 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 { 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 { 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 { generateUrlSearchParams, getUrlParams } 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";
|
||||||
@@ -338,16 +338,6 @@ export async function createRoom(
|
|||||||
|
|
||||||
const result = await createPromise;
|
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;
|
let password;
|
||||||
if (e2ee == E2eeType.SHARED_KEY) {
|
if (e2ee == E2eeType.SHARED_KEY) {
|
||||||
password = secureRandomBase64Url(16);
|
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
|
* 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
|
const roomPart = roomName
|
||||||
// it in case it came from another client that generated a non url-safe one
|
? "/" + roomAliasLocalpartFromRoomName(roomName)
|
||||||
const encodedPassword = password ? encodeURIComponent(password) : undefined;
|
: "";
|
||||||
if (password && encodedPassword !== password) {
|
return `/room/#${roomPart}?${generateUrlSearchParams(roomId, encryptionSystem, viaServers).toString()}`;
|
||||||
logger.info("Encoded call password used non URL-safe chars: buggy client?");
|
|
||||||
}
|
|
||||||
|
|
||||||
return `/room/#${
|
|
||||||
roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : ""
|
|
||||||
}?roomId=${roomId}${password ? "&" + PASSWORD_STRING + encodedPassword : ""}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvatarUrl(
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||||
import styles from "./AppSelectionModal.module.css";
|
import styles from "./AppSelectionModal.module.css";
|
||||||
import { editFragmentQuery } from "../UrlParams";
|
import { editFragmentQuery } from "../UrlParams";
|
||||||
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
roomId: string | null;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||||
@@ -42,10 +43,9 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
|||||||
},
|
},
|
||||||
[setOpen],
|
[setOpen],
|
||||||
);
|
);
|
||||||
|
const e2eeSystem = useRoomEncryptionSystem(roomId);
|
||||||
|
|
||||||
const roomSharedKey = useRoomSharedKey(roomId ?? "");
|
if (e2eeSystem.kind === E2eeType.NONE) {
|
||||||
const roomIsEncrypted = useIsRoomE2EE(roomId ?? "");
|
|
||||||
if (roomIsEncrypted && roomSharedKey === undefined) {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
"Generating app redirect URL for encrypted room but don't have key available!",
|
"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(
|
const url = new URL(
|
||||||
roomId === null
|
roomId === null
|
||||||
? window.location.href
|
? window.location.href
|
||||||
: getAbsoluteRoomUrl(roomId, undefined, roomSharedKey ?? undefined),
|
: getAbsoluteRoomUrl(roomId, e2eeSystem),
|
||||||
);
|
);
|
||||||
// Edit the URL to prevent the app selection prompt from appearing a second
|
// 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
|
// 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:/");
|
const result = new URL("io.element.call:/");
|
||||||
result.searchParams.set("url", url.toString());
|
result.searchParams.set("url", url.toString());
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}, [roomId, roomSharedKey]);
|
}, [e2eeSystem, roomId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -14,22 +14,25 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ReactNode, useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
|
||||||
import { MatrixError } from "matrix-js-sdk";
|
import { MatrixError } from "matrix-js-sdk";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Heading, Link, Text } from "@vector-im/compound-web";
|
import { Heading, Link, Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
import {
|
||||||
|
useLoadGroupCall,
|
||||||
|
GroupCallStatus,
|
||||||
|
CallTerminatedMessage,
|
||||||
|
} from "./useLoadGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
viaServers: string[];
|
viaServers: string[];
|
||||||
children: (rtcSession: MatrixRTCSession) => ReactNode;
|
children: (groupCallState: GroupCallStatus) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallLoader({
|
export function GroupCallLoader({
|
||||||
@@ -51,20 +54,22 @@ export function GroupCallLoader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
switch (groupCallState.kind) {
|
switch (groupCallState.kind) {
|
||||||
|
case "loaded":
|
||||||
|
case "waitForInvite":
|
||||||
|
case "canKnock":
|
||||||
|
return children(groupCallState);
|
||||||
case "loading":
|
case "loading":
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<h1>{t("common.loading")}</h1>
|
<h1>{t("common.loading")}</h1>
|
||||||
</FullScreenView>
|
</FullScreenView>
|
||||||
);
|
);
|
||||||
case "loaded":
|
|
||||||
return <>{children(groupCallState.rtcSession)}</>;
|
|
||||||
case "failed":
|
case "failed":
|
||||||
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<Heading>{t("group_call_loader_failed_heading")}</Heading>
|
<Heading>{t("group_call_loader.failed_heading")}</Heading>
|
||||||
<Text>{t("group_call_loader_failed_text")}</Text>
|
<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
|
{/* 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. */}
|
dupes of this flow, let's make a common component and put it here. */}
|
||||||
<Link href="/" onClick={onHomeClick}>
|
<Link href="/" onClick={onHomeClick}>
|
||||||
@@ -72,6 +77,22 @@ export function GroupCallLoader({
|
|||||||
</Link>
|
</Link>
|
||||||
</FullScreenView>
|
</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 {
|
} else {
|
||||||
return <ErrorView error={groupCallState.error} />;
|
return <ErrorView error={groupCallState.error} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ limitations under the License.
|
|||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
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 type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { MatrixInfo } from "./VideoPreview";
|
import { MatrixInfo } from "./VideoPreview";
|
||||||
import { CallEndedView } from "./CallEndedView";
|
import { CallEndedView } from "./CallEndedView";
|
||||||
@@ -34,17 +37,16 @@ import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
|||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import { findDeviceByName } from "../media-utils";
|
import { findDeviceByName } from "../media-utils";
|
||||||
import { ActiveCall } from "./InCallView";
|
import { ActiveCall } from "./InCallView";
|
||||||
import { MuteStates, useMuteStates } from "./MuteStates";
|
import { MUTE_PARTICIPANT_COUNT, MuteStates } from "./MuteStates";
|
||||||
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
|
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
|
||||||
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
|
||||||
import { useIsRoomE2EE, useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { useRoomAvatar } from "./useRoomAvatar";
|
import { useRoomAvatar } from "./useRoomAvatar";
|
||||||
import { useRoomName } from "./useRoomName";
|
import { useRoomName } from "./useRoomName";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
import { useJoinRule } from "./useJoinRule";
|
||||||
import { InviteModal } from "./InviteModal";
|
import { InviteModal } from "./InviteModal";
|
||||||
import { E2EEConfig } from "../livekit/useLiveKit";
|
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ interface Props {
|
|||||||
skipLobby: boolean;
|
skipLobby: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
|
muteStates: MuteStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupCallView: FC<Props> = ({
|
export const GroupCallView: FC<Props> = ({
|
||||||
@@ -72,10 +75,23 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
skipLobby,
|
skipLobby,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
|
muteStates,
|
||||||
}) => {
|
}) => {
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const isJoined = useMatrixRTCSessionJoinState(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(() => {
|
useEffect(() => {
|
||||||
window.rtcSession = rtcSession;
|
window.rtcSession = rtcSession;
|
||||||
return () => {
|
return () => {
|
||||||
@@ -86,10 +102,8 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const roomName = useRoomName(rtcSession.room);
|
const roomName = useRoomName(rtcSession.room);
|
||||||
const roomAvatar = useRoomAvatar(rtcSession.room);
|
const roomAvatar = useRoomAvatar(rtcSession.room);
|
||||||
const e2eeSharedKey = useRoomSharedKey(rtcSession.room.roomId);
|
|
||||||
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
||||||
const roomEncrypted =
|
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
|
||||||
useIsRoomE2EE(rtcSession.room.roomId) || perParticipantE2EE;
|
|
||||||
|
|
||||||
const matrixInfo = useMemo((): MatrixInfo => {
|
const matrixInfo = useMemo((): MatrixInfo => {
|
||||||
return {
|
return {
|
||||||
@@ -100,16 +114,16 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
roomName,
|
roomName,
|
||||||
roomAlias: rtcSession.room.getCanonicalAlias(),
|
roomAlias: rtcSession.room.getCanonicalAlias(),
|
||||||
roomAvatar,
|
roomAvatar,
|
||||||
roomEncrypted,
|
e2eeSystem,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
client,
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
rtcSession,
|
rtcSession.room,
|
||||||
roomName,
|
roomName,
|
||||||
roomAvatar,
|
roomAvatar,
|
||||||
roomEncrypted,
|
e2eeSystem,
|
||||||
client,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Count each member only once, regardless of how many devices they use
|
// 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>();
|
const latestDevices = useRef<MediaDevices>();
|
||||||
latestDevices.current = deviceContext;
|
latestDevices.current = deviceContext;
|
||||||
|
|
||||||
const muteStates = useMuteStates(memberships.length);
|
|
||||||
const latestMuteStates = useRef<MuteStates>();
|
const latestMuteStates = useRef<MuteStates>();
|
||||||
latestMuteStates.current = 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(() => {
|
useEffect(() => {
|
||||||
const defaultDeviceSetup = async (
|
const defaultDeviceSetup = async (
|
||||||
requestedDeviceData: JoinCallData,
|
requestedDeviceData: JoinCallData,
|
||||||
@@ -288,17 +291,8 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (roomEncrypted && !perParticipantE2EE && !e2eeSharedKey) {
|
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
|
||||||
return (
|
// If we have a encryption system but the browser does not support it.
|
||||||
<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) {
|
|
||||||
return (
|
return (
|
||||||
<FullScreenView>
|
<FullScreenView>
|
||||||
<Heading>{t("browser_media_e2ee_unsupported_heading")}</Heading>
|
<Heading>{t("browser_media_e2ee_unsupported_heading")}</Heading>
|
||||||
@@ -345,7 +339,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
onLeave={onLeave}
|
onLeave={onLeave}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
e2eeConfig={e2eeConfig}
|
e2eeSystem={e2eeSystem}
|
||||||
//otelGroupCallMembership={otelGroupCallMembership}
|
//otelGroupCallMembership={otelGroupCallMembership}
|
||||||
onShareClick={onShareClick}
|
onShareClick={onShareClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
|||||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
import { E2EEConfig, useLiveKit } from "../livekit/useLiveKit";
|
import { useLiveKit } from "../livekit/useLiveKit";
|
||||||
import { useFullscreen } from "./useFullscreen";
|
import { useFullscreen } from "./useFullscreen";
|
||||||
import { useLayoutStates } from "../video-grid/Layout";
|
import { useLayoutStates } from "../video-grid/Layout";
|
||||||
import { useWakeLock } from "../useWakeLock";
|
import { useWakeLock } from "../useWakeLock";
|
||||||
@@ -76,13 +76,15 @@ import { ECConnectionState } from "../livekit/useECConnectionState";
|
|||||||
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
import { useOpenIDSFU } from "../livekit/openIDSFU";
|
||||||
import { useCallViewModel } from "../state/CallViewModel";
|
import { useCallViewModel } from "../state/CallViewModel";
|
||||||
import { subscribe } from "../state/subscribe";
|
import { subscribe } from "../state/subscribe";
|
||||||
|
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
|
||||||
export interface ActiveCallProps
|
export interface ActiveCallProps
|
||||||
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
|
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
|
||||||
e2eeConfig: E2EEConfig;
|
e2eeSystem: EncryptionSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||||
@@ -91,7 +93,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
props.rtcSession,
|
props.rtcSession,
|
||||||
props.muteStates,
|
props.muteStates,
|
||||||
sfuConfig,
|
sfuConfig,
|
||||||
props.e2eeConfig,
|
props.e2eeSystem,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -238,7 +240,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
|||||||
const vm = useCallViewModel(
|
const vm = useCallViewModel(
|
||||||
rtcSession.room,
|
rtcSession.room,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
matrixInfo.roomEncrypted,
|
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
|
||||||
connState,
|
connState,
|
||||||
);
|
);
|
||||||
const items = useStateObservable(vm.tiles);
|
const items = useStateObservable(vm.tiles);
|
||||||
@@ -432,7 +434,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
|||||||
id={matrixInfo.roomId}
|
id={matrixInfo.roomId}
|
||||||
name={matrixInfo.roomName}
|
name={matrixInfo.roomName}
|
||||||
avatarUrl={matrixInfo.roomAvatar}
|
avatarUrl={matrixInfo.roomAvatar}
|
||||||
encrypted={matrixInfo.roomEncrypted}
|
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
/>
|
/>
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import useClipboard from "react-use-clipboard";
|
|||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||||
import styles from "./InviteModal.module.css";
|
import styles from "./InviteModal.module.css";
|
||||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
|
||||||
import { Toast } from "../Toast";
|
import { Toast } from "../Toast";
|
||||||
|
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
room: Room;
|
room: Room;
|
||||||
@@ -36,11 +36,11 @@ interface Props {
|
|||||||
|
|
||||||
export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const roomSharedKey = useRoomSharedKey(room.roomId);
|
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||||
|
|
||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
() =>
|
() => getAbsoluteRoomUrl(room.roomId, e2eeSystem, room.name),
|
||||||
getAbsoluteRoomUrl(room.roomId, room.name, roomSharedKey ?? undefined),
|
[e2eeSystem, room.name, room.roomId],
|
||||||
[room, roomSharedKey],
|
|
||||||
);
|
);
|
||||||
const [, setCopied] = useClipboard(url);
|
const [, setCopied] = useClipboard(url);
|
||||||
const [toastOpen, setToastOpen] = useState(false);
|
const [toastOpen, setToastOpen] = useState(false);
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ limitations under the License.
|
|||||||
height: 100%;
|
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) {
|
@media (max-width: 500px) {
|
||||||
.join {
|
.join {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import { Button, Link } from "@vector-im/compound-web";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import styles from "./LobbyView.module.css";
|
|
||||||
import inCallStyles from "./InCallView.module.css";
|
import inCallStyles from "./InCallView.module.css";
|
||||||
|
import styles from "./LobbyView.module.css";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
import { MatrixInfo, VideoPreview } from "./VideoPreview";
|
||||||
@@ -36,16 +36,19 @@ import {
|
|||||||
} from "../button/Button";
|
} from "../button/Button";
|
||||||
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
|
||||||
import { useMediaQuery } from "../useMediaQuery";
|
import { useMediaQuery } from "../useMediaQuery";
|
||||||
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
matrixInfo: MatrixInfo;
|
matrixInfo: MatrixInfo;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
onEnter: () => void;
|
onEnter: () => void;
|
||||||
|
enterLabel?: JSX.Element | string;
|
||||||
confineToRoom: boolean;
|
confineToRoom: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
participantCount: number;
|
participantCount: number | null;
|
||||||
onShareClick: (() => void) | null;
|
onShareClick: (() => void) | null;
|
||||||
|
waitingForInvite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LobbyView: FC<Props> = ({
|
export const LobbyView: FC<Props> = ({
|
||||||
@@ -53,10 +56,12 @@ export const LobbyView: FC<Props> = ({
|
|||||||
matrixInfo,
|
matrixInfo,
|
||||||
muteStates,
|
muteStates,
|
||||||
onEnter,
|
onEnter,
|
||||||
|
enterLabel,
|
||||||
confineToRoom,
|
confineToRoom,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
participantCount,
|
participantCount,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
|
waitingForInvite,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useLocationNavigation();
|
useLocationNavigation();
|
||||||
@@ -104,7 +109,7 @@ export const LobbyView: FC<Props> = ({
|
|||||||
id={matrixInfo.roomId}
|
id={matrixInfo.roomId}
|
||||||
name={matrixInfo.roomName}
|
name={matrixInfo.roomName}
|
||||||
avatarUrl={matrixInfo.roomAvatar}
|
avatarUrl={matrixInfo.roomAvatar}
|
||||||
encrypted={matrixInfo.roomEncrypted}
|
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
/>
|
/>
|
||||||
</LeftNav>
|
</LeftNav>
|
||||||
@@ -116,12 +121,16 @@ export const LobbyView: FC<Props> = ({
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates}>
|
<VideoPreview matrixInfo={matrixInfo} muteStates={muteStates}>
|
||||||
<Button
|
<Button
|
||||||
className={styles.join}
|
className={classNames(styles.join, {
|
||||||
size="lg"
|
[styles.wait]: waitingForInvite,
|
||||||
onClick={onEnter}
|
})}
|
||||||
|
size={waitingForInvite ? "sm" : "lg"}
|
||||||
|
onClick={() => {
|
||||||
|
if (!waitingForInvite) onEnter();
|
||||||
|
}}
|
||||||
data-testid="lobby_joinCall"
|
data-testid="lobby_joinCall"
|
||||||
>
|
>
|
||||||
{t("lobby.join_button")}
|
{enterLabel ?? t("lobby.join_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</VideoPreview>
|
</VideoPreview>
|
||||||
{!recentsButtonInFooter && recentsButton}
|
{!recentsButtonInFooter && recentsButton}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext";
|
|||||||
import { useReactiveState } from "../useReactiveState";
|
import { useReactiveState } from "../useReactiveState";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If there already is this many participants in the call, we automatically mute
|
* If there already are this many participants in the call, we automatically mute
|
||||||
* the user
|
* the user.
|
||||||
*/
|
*/
|
||||||
const MUTE_PARTICIPANT_COUNT = 8;
|
export const MUTE_PARTICIPANT_COUNT = 8;
|
||||||
|
|
||||||
interface DeviceAvailable {
|
interface DeviceAvailable {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -51,26 +51,27 @@ function useMuteState(
|
|||||||
device: MediaDevice,
|
device: MediaDevice,
|
||||||
enabledByDefault: () => boolean,
|
enabledByDefault: () => boolean,
|
||||||
): MuteState {
|
): MuteState {
|
||||||
const [enabled, setEnabled] = useReactiveState<boolean>(
|
const [enabled, setEnabled] = useReactiveState<boolean | undefined>(
|
||||||
(prev) => device.available.length > 0 && (prev ?? enabledByDefault()),
|
(prev) =>
|
||||||
|
device.available.length > 0 ? prev ?? enabledByDefault() : undefined,
|
||||||
[device],
|
[device],
|
||||||
);
|
);
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
device.available.length === 0
|
device.available.length === 0
|
||||||
? deviceUnavailable
|
? deviceUnavailable
|
||||||
: { enabled, setEnabled },
|
: {
|
||||||
|
enabled: enabled ?? false,
|
||||||
|
setEnabled: setEnabled as Dispatch<SetStateAction<boolean>>,
|
||||||
|
},
|
||||||
[device, enabled, setEnabled],
|
[device, enabled, setEnabled],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMuteStates(participantCount: number): MuteStates {
|
export function useMuteStates(): MuteStates {
|
||||||
const devices = useMediaDevices();
|
const devices = useMediaDevices();
|
||||||
|
|
||||||
const audio = useMuteState(
|
const audio = useMuteState(devices.audioInput, () => true);
|
||||||
devices.audioInput,
|
|
||||||
() => participantCount <= MUTE_PARTICIPANT_COUNT,
|
|
||||||
);
|
|
||||||
const video = useMuteState(devices.videoInput, () => true);
|
const video = useMuteState(devices.videoInput, () => true);
|
||||||
|
|
||||||
return useMemo(() => ({ audio, video }), [audio, video]);
|
return useMemo(() => ({ audio, video }), [audio, video]);
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
|
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 { 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 { useClientLegacy } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
@@ -30,6 +31,11 @@ import { HomePage } from "../home/HomePage";
|
|||||||
import { platform } from "../Platform";
|
import { platform } from "../Platform";
|
||||||
import { AppSelectionModal } from "./AppSelectionModal";
|
import { AppSelectionModal } from "./AppSelectionModal";
|
||||||
import { widget } from "../widget";
|
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 = () => {
|
export const RoomPage: FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -40,7 +46,7 @@ export const RoomPage: FC = () => {
|
|||||||
displayName,
|
displayName,
|
||||||
skipLobby,
|
skipLobby,
|
||||||
} = useUrlParams();
|
} = useUrlParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { roomAlias, roomId, viaServers } = useRoomIdentifier();
|
const { roomAlias, roomId, viaServers } = useRoomIdentifier();
|
||||||
|
|
||||||
const roomIdOrAlias = roomId ?? roomAlias;
|
const roomIdOrAlias = roomId ?? roomAlias;
|
||||||
@@ -48,17 +54,14 @@ export const RoomPage: FC = () => {
|
|||||||
logger.error("No room specified");
|
logger.error("No room specified");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
|
||||||
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
||||||
const [isRegistering, setIsRegistering] = useState(false);
|
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 } =
|
const { loading, authenticated, client, error, passwordlessUser } =
|
||||||
useClientLegacy();
|
useClientLegacy();
|
||||||
|
const { avatarUrl, displayName: userDisplayName } = useProfile(client);
|
||||||
|
|
||||||
|
const muteStates = useMuteStates();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If we've finished loading, are not already authed and we've been given a display name as
|
// 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,
|
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(
|
const groupCallView = useCallback(
|
||||||
(rtcSession: MatrixRTCSession) => (
|
(groupCallState: GroupCallStatus): JSX.Element => {
|
||||||
<GroupCallView
|
switch (groupCallState.kind) {
|
||||||
client={client!}
|
case "loaded":
|
||||||
rtcSession={rtcSession}
|
return (
|
||||||
isPasswordlessUser={passwordlessUser}
|
<GroupCallView
|
||||||
confineToRoom={confineToRoom}
|
client={client!}
|
||||||
preload={preload}
|
rtcSession={groupCallState.rtcSession}
|
||||||
skipLobby={skipLobby}
|
isPasswordlessUser={passwordlessUser}
|
||||||
hideHeader={hideHeader}
|
confineToRoom={confineToRoom}
|
||||||
/>
|
preload={preload}
|
||||||
),
|
skipLobby={skipLobby}
|
||||||
[client, passwordlessUser, confineToRoom, preload, hideHeader, 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;
|
let content: ReactNode;
|
||||||
@@ -118,9 +189,9 @@ export const RoomPage: FC = () => {
|
|||||||
<>
|
<>
|
||||||
{content}
|
{content}
|
||||||
{/* On Android and iOS, show a prompt to launch the mobile app. */}
|
{/* On Android and iOS, show a prompt to launch the mobile app. */}
|
||||||
{appPrompt && (platform === "android" || platform === "ios") && (
|
{appPrompt &&
|
||||||
<AppSelectionModal roomId={roomId} />
|
(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.
|
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 useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
import { usePreviewTracks } from "@livekit/components-react";
|
import { usePreviewTracks } from "@livekit/components-react";
|
||||||
@@ -32,6 +32,7 @@ import styles from "./VideoPreview.module.css";
|
|||||||
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||||
import { MuteStates } from "./MuteStates";
|
import { MuteStates } from "./MuteStates";
|
||||||
import { useMediaQuery } from "../useMediaQuery";
|
import { useMediaQuery } from "../useMediaQuery";
|
||||||
|
import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
|
|
||||||
export type MatrixInfo = {
|
export type MatrixInfo = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -41,7 +42,7 @@ export type MatrixInfo = {
|
|||||||
roomName: string;
|
roomName: string;
|
||||||
roomAlias: string | null;
|
roomAlias: string | null;
|
||||||
roomAvatar: string | null;
|
roomAvatar: string | null;
|
||||||
roomEncrypted: boolean;
|
e2eeSystem: EncryptionSystem;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -67,8 +68,8 @@ export const VideoPreview: FC<Props> = ({
|
|||||||
deviceId: devices.audioInput.selectedId,
|
deviceId: devices.audioInput.selectedId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracks = usePreviewTracks(
|
const localTrackOptions = useMemo(
|
||||||
{
|
() => ({
|
||||||
// The only reason we request audio here is to get the audio permission
|
// 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
|
// 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
|
// 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 && {
|
video: muteStates.video.enabled && {
|
||||||
deviceId: devices.videoInput.selectedId,
|
deviceId: devices.videoInput.selectedId,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
(error) => {
|
[devices.videoInput.selectedId, muteStates.video.enabled],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onError = useCallback(
|
||||||
|
(error: Error) => {
|
||||||
logger.error("Error while creating preview Tracks:", error);
|
logger.error("Error while creating preview Tracks:", error);
|
||||||
muteStates.audio.setEnabled?.(false);
|
muteStates.audio.setEnabled?.(false);
|
||||||
muteStates.video.setEnabled?.(false);
|
muteStates.video.setEnabled?.(false);
|
||||||
},
|
},
|
||||||
|
[muteStates.audio, muteStates.video],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tracks = usePreviewTracks(localTrackOptions, onError);
|
||||||
|
|
||||||
const videoTrack = useMemo(
|
const videoTrack = useMemo(
|
||||||
() =>
|
() =>
|
||||||
tracks?.find((t) => t.kind === Track.Kind.Video) as
|
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.
|
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 { 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 { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
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 { widget } from "../widget";
|
||||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
|
||||||
|
|
||||||
export type GroupCallLoaded = {
|
export type GroupCallLoaded = {
|
||||||
kind: "loaded";
|
kind: "loaded";
|
||||||
@@ -38,14 +45,48 @@ export type GroupCallLoading = {
|
|||||||
kind: "loading";
|
kind: "loading";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GroupCallWaitForInvite = {
|
||||||
|
kind: "waitForInvite";
|
||||||
|
roomSummary: RoomSummary;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupCallCanKnock = {
|
||||||
|
kind: "canKnock";
|
||||||
|
roomSummary: RoomSummary;
|
||||||
|
knock: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type GroupCallStatus =
|
export type GroupCallStatus =
|
||||||
| GroupCallLoaded
|
| GroupCallLoaded
|
||||||
| GroupCallLoadFailed
|
| GroupCallLoadFailed
|
||||||
| GroupCallLoading;
|
| GroupCallLoading
|
||||||
|
| GroupCallWaitForInvite
|
||||||
|
| GroupCallCanKnock;
|
||||||
|
|
||||||
export interface GroupCallLoadState {
|
export class CallTerminatedMessage extends Error {
|
||||||
error?: Error;
|
/**
|
||||||
groupCall?: GroupCall;
|
* @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 = (
|
export const useLoadGroupCall = (
|
||||||
@@ -53,36 +94,159 @@ export const useLoadGroupCall = (
|
|||||||
roomIdOrAlias: string,
|
roomIdOrAlias: string,
|
||||||
viaServers: string[],
|
viaServers: string[],
|
||||||
): GroupCallStatus => {
|
): GroupCallStatus => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
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(() => {
|
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> => {
|
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||||
let room: Room | null = null;
|
let room: Room | null = null;
|
||||||
if (roomIdOrAlias[0] === "#") {
|
if (roomIdOrAlias[0] === "#") {
|
||||||
// We lowercase the localpart when we create the room, so we must lowercase
|
const alias = roomIdOrAlias;
|
||||||
// it here too (we just do the whole alias). We can't do the same to room IDs
|
// The call uses a room alias
|
||||||
// though.
|
room = await getRoomByAlias(alias);
|
||||||
// Also, we explicitly look up the room alias here. We previously just tried to
|
activeRoom.current = room;
|
||||||
// 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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} 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(
|
logger.info(
|
||||||
@@ -95,6 +259,7 @@ export const useLoadGroupCall = (
|
|||||||
|
|
||||||
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
||||||
const room = await fetchOrCreateRoom();
|
const room = await fetchOrCreateRoom();
|
||||||
|
activeRoom.current = room;
|
||||||
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
logger.debug(`Fetched / joined room ${roomIdOrAlias}`);
|
||||||
|
|
||||||
const rtcSession = client.matrixRTC.getRoomSession(room);
|
const rtcSession = client.matrixRTC.getRoomSession(room);
|
||||||
@@ -119,11 +284,33 @@ export const useLoadGroupCall = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
waitForClientSyncing()
|
const observeMyMembership = async (): Promise<void> => {
|
||||||
.then(fetchOrCreateGroupCall)
|
await new Promise((_, reject) => {
|
||||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
client.on(RoomEvent.MyMembership, async (_, membership) => {
|
||||||
.catch((error) => setState({ kind: "failed", error }));
|
if (membership === KnownMembership.Leave) reject(removeNoticeError());
|
||||||
}, [client, roomIdOrAlias, viaServers, t]);
|
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;
|
return state;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -298,13 +298,13 @@ export function useRageshakeRequest(): (
|
|||||||
|
|
||||||
const sendRageshakeRequest = useCallback(
|
const sendRageshakeRequest = useCallback(
|
||||||
(roomId: string, rageshakeRequestId: string) => {
|
(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", {
|
client!.sendEvent(roomId, "org.matrix.rageshake_request", {
|
||||||
request_id: rageshakeRequestId,
|
request_id: rageshakeRequestId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client],
|
[client],
|
||||||
);
|
);
|
||||||
|
|
||||||
return sendRageshakeRequest;
|
return sendRageshakeRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export const widget = ((): WidgetHelpers | null => {
|
|||||||
];
|
];
|
||||||
const receiveState = [
|
const receiveState = [
|
||||||
{ eventType: EventType.RoomMember },
|
{ eventType: EventType.RoomMember },
|
||||||
|
{ eventType: EventType.RoomEncryption },
|
||||||
{ eventType: EventType.GroupCallPrefix },
|
{ eventType: EventType.GroupCallPrefix },
|
||||||
{ eventType: EventType.GroupCallMemberPrefix },
|
{ eventType: EventType.GroupCallMemberPrefix },
|
||||||
];
|
];
|
||||||
|
|||||||
56
yarn.lock
56
yarn.lock
@@ -1120,9 +1120,9 @@
|
|||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@bufbuild/protobuf@^1.7.2":
|
"@bufbuild/protobuf@^1.7.2":
|
||||||
version "1.8.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.8.0.tgz#1c8651ea34adb8019b483e09de02aeeb1cd57d79"
|
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.9.0.tgz#fffac3183059a41ceef5311e07e3724d426a95c4"
|
||||||
integrity sha512-qR9FwI8QKIveDnUYutvfzbC21UZJJryYrLuZGjeZ/VGz+vXelUkK+xgkOHsvPEdYEdxtgUUq4313N8QtOehJ1Q==
|
integrity sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ==
|
||||||
|
|
||||||
"@csstools/cascade-layer-name-parser@^1.0.8":
|
"@csstools/cascade-layer-name-parser@^1.0.8":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||||
|
|
||||||
"@livekit/components-core@0.9.3":
|
"@livekit/components-core@0.10.0", "@livekit/components-core@^0.10.0":
|
||||||
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":
|
|
||||||
version "0.10.0"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@livekit/components-core/-/components-core-0.10.0.tgz#dfd1ccf72518d89bba2d9d10fadbbf2dd47ed321"
|
resolved "https://registry.yarnpkg.com/@livekit/components-core/-/components-core-0.10.0.tgz#dfd1ccf72518d89bba2d9d10fadbbf2dd47ed321"
|
||||||
integrity sha512-TSsIG2BRLABT5FP+5sueZgkByGYyFhv3UTb8fneWchvQRBHtiU9s4FF8SIoAw9z3znhwp1tKaJyuIyKp7k0Juw==
|
integrity sha512-TSsIG2BRLABT5FP+5sueZgkByGYyFhv3UTb8fneWchvQRBHtiU9s4FF8SIoAw9z3znhwp1tKaJyuIyKp7k0Juw==
|
||||||
@@ -1739,11 +1729,11 @@
|
|||||||
rxjs "7.8.1"
|
rxjs "7.8.1"
|
||||||
|
|
||||||
"@livekit/components-react@^2.0.0":
|
"@livekit/components-react@^2.0.0":
|
||||||
version "2.0.6"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.0.6.tgz#c4ff790180f86f6a3cfc341fe5c0b93bac8bf92b"
|
resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.2.0.tgz#a2c9a1568055cf07d4784b98336f28d574638bf8"
|
||||||
integrity sha512-L0iaIPasPJLftI6FBcWcGicmGpw5LKKFvMV6GSyuv0q8oYrmUYoLLQdGmw7eW4q+7bQFAcFHeD9BzMXdprzCgw==
|
integrity sha512-TDa2YNBphkdf2dz85pEZs1UBl8wD/LHFeYupNoTqjtlLVlTXpr09Buv3/eegQFJhXoDSK6fAYqKZ4U/oYydv/w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@livekit/components-core" "0.9.3"
|
"@livekit/components-core" "0.10.0"
|
||||||
"@react-hook/latest" "1.0.3"
|
"@react-hook/latest" "1.0.3"
|
||||||
clsx "2.1.0"
|
clsx "2.1.0"
|
||||||
usehooks-ts "2.16.0"
|
usehooks-ts "2.16.0"
|
||||||
@@ -1755,10 +1745,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@bufbuild/protobuf" "^1.7.2"
|
"@bufbuild/protobuf" "^1.7.2"
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm@^4.6.0":
|
"@matrix-org/matrix-sdk-crypto-wasm@^4.9.0":
|
||||||
version "4.6.0"
|
version "4.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.6.0.tgz#35224214c7638abbe2bc91fb4fa4fb022a1a2bf0"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-4.9.0.tgz#9dfed83e33f760650596c4e5c520e5e4c53355d2"
|
||||||
integrity sha512-v9PFWzSTWMlZKbyk3PPsZjUtOEQ7FIz5USD3lFRUWiS4pv0FOKR125VOUnR5Z/kAty57JXCHDAexCln3zE2Fww==
|
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":
|
"@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"
|
version "3.2.14"
|
||||||
@@ -3289,9 +3279,9 @@
|
|||||||
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
|
||||||
|
|
||||||
"@types/node@*", "@types/node@^20.0.0":
|
"@types/node@*", "@types/node@^20.0.0":
|
||||||
version "20.12.6"
|
version "20.12.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.6.tgz#72d068870518d7da1d97b49db401e2d6a1805294"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384"
|
||||||
integrity sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==
|
integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
@@ -6164,9 +6154,9 @@ lines-and-columns@^1.1.6:
|
|||||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||||
|
|
||||||
livekit-client@^2.0.2:
|
livekit-client@^2.0.2:
|
||||||
version "2.1.0"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.1.0.tgz#3c4f2754eb38933a6d232fe35717bfe3a378bdcf"
|
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.1.1.tgz#c3e1cc2f11727b7a760c801ba98a97585dda031f"
|
||||||
integrity sha512-nJwfRKw1Pafd2napk66l30dlBjsv1VZ+na3mzNezcAFAYT2lQ4Gch57TdbMBDYo+QfrZ98s+kuZzsFhBwM5rqw==
|
integrity sha512-ffnXHQt210GPJ9sR846o7g0lCg/3TJqZxdu55mzQFS1YXGgn9PYKGzcAhKtuOsQ0NEkkn1zKQ0ABHBt7iADiqg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@livekit/protocol" "1.13.0"
|
"@livekit/protocol" "1.13.0"
|
||||||
events "^3.3.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"
|
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
||||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#d55c6a36df539f6adacc335efe5b9be27c9cee4a":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e874468ba3e84819cf4b342d2e66af67ab4cf804":
|
||||||
version "31.4.0"
|
version "32.0.0"
|
||||||
uid d55c6a36df539f6adacc335efe5b9be27c9cee4a
|
uid e874468ba3e84819cf4b342d2e66af67ab4cf804
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d55c6a36df539f6adacc335efe5b9be27c9cee4a"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e874468ba3e84819cf4b342d2e66af67ab4cf804"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@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"
|
another-json "^0.2.0"
|
||||||
bs58 "^5.0.0"
|
bs58 "^5.0.0"
|
||||||
content-type "^1.0.4"
|
content-type "^1.0.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user