mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
commit
1dfffce606
@ -38,7 +38,8 @@
|
||||
"classnames": "^2.3.1",
|
||||
"color-hash": "^2.0.1",
|
||||
"events": "^3.3.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8ba2d257ae24bbed61cd7fe99af081324337161c",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#3334c01191bcd82b5243916284c9a08d08fd9795",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pako": "^2.0.4",
|
||||
|
@ -26,9 +26,14 @@ import React, {
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { ErrorView } from "./FullScreenView";
|
||||
import { initClient, defaultHomeserver } from "./matrix-utils";
|
||||
import {
|
||||
initClient,
|
||||
initMatroskaClient,
|
||||
defaultHomeserver,
|
||||
} from "./matrix-utils";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -91,40 +96,55 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const restore = async (): Promise<
|
||||
const init = async (): Promise<
|
||||
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||
> => {
|
||||
try {
|
||||
const session = loadSession();
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const widgetId = query.get("widgetId");
|
||||
const parentUrl = query.get("parentUrl");
|
||||
|
||||
if (session) {
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } =
|
||||
session;
|
||||
if (widgetId && parentUrl) {
|
||||
// We're inside a widget, so let's engage *Matroska mode*
|
||||
logger.log("Using a Matroska client");
|
||||
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
true
|
||||
);
|
||||
/* eslint-enable camelcase */
|
||||
return {
|
||||
client: await initMatroskaClient(widgetId, parentUrl),
|
||||
isPasswordlessUser: false,
|
||||
};
|
||||
} else {
|
||||
// We're running as a standalone application
|
||||
try {
|
||||
const session = loadSession();
|
||||
|
||||
return { client, isPasswordlessUser: passwordlessUser };
|
||||
if (session) {
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } =
|
||||
session;
|
||||
|
||||
logger.log("Using a standalone client");
|
||||
const client = await initClient(
|
||||
{
|
||||
baseUrl: defaultHomeserver,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
},
|
||||
true
|
||||
);
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
return { client, isPasswordlessUser: passwordlessUser };
|
||||
}
|
||||
|
||||
return { client: undefined, isPasswordlessUser: false };
|
||||
} catch (err) {
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { client: undefined, isPasswordlessUser: false };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
clearSession();
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
restore()
|
||||
init()
|
||||
.then(({ client, isPasswordlessUser }) => {
|
||||
setState({
|
||||
client,
|
||||
@ -135,7 +155,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
setState({
|
||||
client: undefined,
|
||||
loading: false,
|
||||
|
@ -37,7 +37,7 @@ import { AriaDialogProps } from "@react-types/dialog";
|
||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||
import styles from "./Modal.module.css";
|
||||
|
||||
interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||
export interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
|
@ -5,14 +5,21 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
createClient,
|
||||
createRoomWidgetClient,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||
import { getRoomParams } from "./room/useRoomParams";
|
||||
|
||||
export const defaultHomeserver =
|
||||
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
|
||||
@ -53,7 +60,74 @@ function waitForSync(client: MatrixClient) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises and returns a new Matrix Client
|
||||
* Initialises and returns a new widget-API-based Matrix Client.
|
||||
* @param widgetId The ID of the widget that the app is running inside.
|
||||
* @param parentUrl The URL of the parent client.
|
||||
* @returns The MatrixClient instance
|
||||
*/
|
||||
export async function initMatroskaClient(
|
||||
widgetId: string,
|
||||
parentUrl: string
|
||||
): Promise<MatrixClient> {
|
||||
// In this mode, we use a special client which routes all requests through
|
||||
// the host application via the widget API
|
||||
|
||||
const { roomId, userId, deviceId } = getRoomParams();
|
||||
if (!roomId) throw new Error("Room ID must be supplied");
|
||||
if (!userId) throw new Error("User ID must be supplied");
|
||||
if (!deviceId) throw new Error("Device ID must be supplied");
|
||||
|
||||
// These are all the event types the app uses
|
||||
const sendState = [
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
||||
];
|
||||
const receiveState = [
|
||||
{ eventType: EventType.RoomMember },
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix },
|
||||
];
|
||||
const sendRecvToDevice = [
|
||||
EventType.CallInvite,
|
||||
EventType.CallCandidates,
|
||||
EventType.CallAnswer,
|
||||
EventType.CallHangup,
|
||||
EventType.CallReject,
|
||||
EventType.CallSelectAnswer,
|
||||
EventType.CallNegotiate,
|
||||
EventType.CallSDPStreamMetadataChanged,
|
||||
EventType.CallSDPStreamMetadataChangedPrefix,
|
||||
EventType.CallReplaces,
|
||||
"org.matrix.call_duplicate_session",
|
||||
];
|
||||
|
||||
// Since all data should be coming from the host application, there's no
|
||||
// need to persist anything, and therefore we can use the default stores
|
||||
// We don't even need to set up crypto
|
||||
const client = createRoomWidgetClient(
|
||||
new WidgetApi(widgetId, new URL(parentUrl).origin),
|
||||
{
|
||||
sendState,
|
||||
receiveState,
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: true,
|
||||
},
|
||||
roomId,
|
||||
{
|
||||
baseUrl: "",
|
||||
userId,
|
||||
deviceId,
|
||||
timelineSupport: true,
|
||||
}
|
||||
);
|
||||
|
||||
await client.startClient();
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises and returns a new standalone Matrix Client.
|
||||
* If true is passed for the 'restore' parameter, a check will be made
|
||||
* to ensure that corresponding crypto data is stored and recovered.
|
||||
* If the check fails, CryptoStoreIntegrityError will be thrown.
|
||||
@ -127,16 +201,13 @@ export async function initClient(
|
||||
storeOpts.cryptoStore = new MemoryCryptoStore();
|
||||
}
|
||||
|
||||
// XXX: we read from the URL search params in RoomPage too:
|
||||
// XXX: we read from the room params in RoomPage too:
|
||||
// it would be much better to read them in one place and pass
|
||||
// the values around, but we initialise the matrix client in
|
||||
// many different places so we'd have to pass it into all of
|
||||
// them.
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
// disable e2e only if enableE2e=false is given
|
||||
const enableE2e = params.get("enableE2e") !== "false";
|
||||
|
||||
if (!enableE2e) {
|
||||
const { e2eEnabled } = getRoomParams();
|
||||
if (!e2eEnabled) {
|
||||
logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
|
||||
}
|
||||
|
||||
@ -144,10 +215,10 @@ export async function initClient(
|
||||
...storeOpts,
|
||||
...clientOptions,
|
||||
useAuthorizationHeader: true,
|
||||
// Use a relatively low timeout for API calls: this is a realtime application
|
||||
// Use a relatively low timeout for API calls: this is a realtime app
|
||||
// so we don't want API calls taking ages, we'd rather they just fail.
|
||||
localTimeoutMs: 5000,
|
||||
useE2eForGroupCall: enableE2e,
|
||||
useE2eForGroupCall: e2eEnabled,
|
||||
});
|
||||
|
||||
try {
|
||||
@ -254,17 +325,17 @@ export async function createRoom(
|
||||
return [fullAliasFromRoomName(name, client), result.room_id];
|
||||
}
|
||||
|
||||
export function getRoomUrl(roomId: string): string {
|
||||
if (roomId.startsWith("#")) {
|
||||
const [localPart, host] = roomId.replace("#", "").split(":");
|
||||
export function getRoomUrl(roomIdOrAlias: string): string {
|
||||
if (roomIdOrAlias.startsWith("#")) {
|
||||
const [localPart, host] = roomIdOrAlias.replace("#", "").split(":");
|
||||
|
||||
if (host !== defaultHomeserverHost) {
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`;
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/${localPart}`;
|
||||
}
|
||||
} else {
|
||||
return `${window.location.protocol}//${window.location.host}/room/${roomId}`;
|
||||
return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
viaServers: string[];
|
||||
children: (groupCall: GroupCall) => ReactNode;
|
||||
createPtt: boolean;
|
||||
@ -31,14 +31,14 @@ interface Props {
|
||||
|
||||
export function GroupCallLoader({
|
||||
client,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
viaServers,
|
||||
children,
|
||||
createPtt,
|
||||
}: Props): JSX.Element {
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
viaServers,
|
||||
createPtt
|
||||
);
|
||||
|
@ -37,14 +37,14 @@ interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
isEmbedded: boolean;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
isEmbedded,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
groupCall,
|
||||
}: Props) {
|
||||
const {
|
||||
@ -101,7 +101,7 @@ export function GroupCallView({
|
||||
return (
|
||||
<PTTCallView
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
groupCall={groupCall}
|
||||
@ -129,7 +129,7 @@ export function GroupCallView({
|
||||
isScreensharing={isScreensharing}
|
||||
localScreenshareFeed={localScreenshareFeed}
|
||||
screenshareFeeds={screenshareFeeds}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||
/>
|
||||
);
|
||||
@ -164,7 +164,7 @@ export function GroupCallView({
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
);
|
||||
|
@ -71,7 +71,7 @@ interface Props {
|
||||
isScreensharing: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localScreenshareFeed: CallFeed;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ export function InCallView({
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
localScreenshareFeed,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
unencryptedEventsFromUsers,
|
||||
}: Props) {
|
||||
usePreventScroll();
|
||||
@ -260,7 +260,7 @@ export function InCallView({
|
||||
{!fullscreenParticipant && (
|
||||
<OverflowMenu
|
||||
inCall
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
showInvite={true}
|
||||
feedbackModalState={feedbackModalState}
|
||||
@ -277,7 +277,7 @@ export function InCallView({
|
||||
{rageshakeRequestModalState.isOpen && (
|
||||
<RageshakeRequestModal
|
||||
{...rageshakeRequestModalProps}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -14,31 +14,30 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
|
||||
export function InviteModal({
|
||||
roomId,
|
||||
...rest
|
||||
}: {
|
||||
roomId: string;
|
||||
[x: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
title="Invite People"
|
||||
isDismissable
|
||||
className={styles.inviteModal}
|
||||
{...rest}
|
||||
>
|
||||
<ModalContent>
|
||||
<p>Copy and share this meeting link</p>
|
||||
<CopyButton className={styles.copyButton} value={getRoomUrl(roomId)} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
roomIdOrAlias: string;
|
||||
}
|
||||
|
||||
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
|
||||
<Modal
|
||||
title="Invite People"
|
||||
isDismissable
|
||||
className={styles.inviteModal}
|
||||
{...rest}
|
||||
>
|
||||
<ModalContent>
|
||||
<p>Copy and share this meeting link</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomIdOrAlias)}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -45,7 +45,7 @@ interface Props {
|
||||
toggleLocalVideoMuted: () => void;
|
||||
toggleMicrophoneMuted: () => void;
|
||||
localVideoMuted: boolean;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
isEmbedded: boolean;
|
||||
}
|
||||
export function LobbyView({
|
||||
@ -61,7 +61,7 @@ export function LobbyView({
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
isEmbedded,
|
||||
}: Props) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
@ -115,7 +115,7 @@ export function LobbyView({
|
||||
<VideoPreview
|
||||
state={state}
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
@ -136,7 +136,7 @@ export function LobbyView({
|
||||
<Body>Or</Body>
|
||||
<CopyButton
|
||||
variant="secondaryCopy"
|
||||
value={getRoomUrl(roomId)}
|
||||
value={getRoomUrl(roomIdOrAlias)}
|
||||
className={styles.copyButton}
|
||||
copiedMessage="Call link copied"
|
||||
>
|
||||
|
@ -32,7 +32,7 @@ import { InviteModal } from "./InviteModal";
|
||||
import { TooltipTrigger } from "../Tooltip";
|
||||
import { FeedbackModal } from "./FeedbackModal";
|
||||
interface Props {
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
inCall: boolean;
|
||||
groupCall: GroupCall;
|
||||
showInvite: boolean;
|
||||
@ -43,7 +43,7 @@ interface Props {
|
||||
};
|
||||
}
|
||||
export function OverflowMenu({
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
inCall,
|
||||
groupCall,
|
||||
showInvite,
|
||||
@ -119,7 +119,7 @@ export function OverflowMenu({
|
||||
</PopoverMenuTrigger>
|
||||
{settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal roomId={roomId} {...inviteModalProps} />
|
||||
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
|
||||
)}
|
||||
{feedbackModalState.isOpen && (
|
||||
<FeedbackModal
|
||||
|
@ -87,7 +87,7 @@ function getPromptText(
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
groupCall: GroupCall;
|
||||
@ -99,7 +99,7 @@ interface Props {
|
||||
|
||||
export const PTTCallView: React.FC<Props> = ({
|
||||
client,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
roomName,
|
||||
avatarUrl,
|
||||
groupCall,
|
||||
@ -205,7 +205,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
<div className={styles.footer}>
|
||||
<OverflowMenu
|
||||
inCall
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
showInvite={false}
|
||||
feedbackModalState={feedbackModalState}
|
||||
@ -282,7 +282,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{inviteModalState.isOpen && showControls && (
|
||||
<InviteModal roomId={roomId} {...inviteModalProps} />
|
||||
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -14,24 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { FC, useEffect } from "react";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Modal, ModalContent, ModalProps } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||
import { Body } from "../typography/Typography";
|
||||
|
||||
export function RageshakeRequestModal({
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
...rest
|
||||
}: {
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
rageshakeRequestId: string;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
onClose: () => void;
|
||||
[x: string]: unknown;
|
||||
}) {
|
||||
}
|
||||
|
||||
export const RageshakeRequestModal: FC<Props> = ({
|
||||
rageshakeRequestId,
|
||||
roomIdOrAlias,
|
||||
...rest
|
||||
}) => {
|
||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||
|
||||
useEffect(() => {
|
||||
@ -53,7 +54,7 @@ export function RageshakeRequestModal({
|
||||
submitRageshake({
|
||||
sendLogs: true,
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
|
||||
})
|
||||
}
|
||||
disabled={sending}
|
||||
@ -69,4 +70,4 @@ export function RageshakeRequestModal({
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
Copyright 2021-2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,33 +14,26 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
|
||||
import { useClient } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
import { GroupCallLoader } from "./GroupCallLoader";
|
||||
import { GroupCallView } from "./GroupCallView";
|
||||
import { useRoomParams } from "./useRoomParams";
|
||||
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||
|
||||
export function RoomPage() {
|
||||
export const RoomPage: FC = () => {
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search }: { hash: string; search: string } = useLocation();
|
||||
const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => {
|
||||
const params = new URLSearchParams(search);
|
||||
return [
|
||||
params.getAll("via"),
|
||||
params.has("embed"),
|
||||
params.get("ptt") === "true",
|
||||
params.get("displayName"),
|
||||
];
|
||||
}, [search]);
|
||||
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
||||
const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } =
|
||||
useRoomParams();
|
||||
const roomIdOrAlias = roomId ?? roomAlias;
|
||||
if (!roomIdOrAlias) throw new Error("No room specified");
|
||||
|
||||
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
|
||||
@ -76,14 +69,14 @@ export function RoomPage() {
|
||||
<MediaHandlerProvider client={client}>
|
||||
<GroupCallLoader
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
viaServers={viaServers}
|
||||
createPtt={isPtt}
|
||||
>
|
||||
{(groupCall) => (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isEmbedded={isEmbedded}
|
||||
@ -92,4 +85,4 @@ export function RoomPage() {
|
||||
</GroupCallLoader>
|
||||
</MediaHandlerProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ import { useModalTriggerState } from "../Modal";
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
state: GroupCallState;
|
||||
roomId: string;
|
||||
roomIdOrAlias: string;
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
toggleLocalVideoMuted: () => void;
|
||||
@ -43,7 +43,7 @@ interface Props {
|
||||
export function VideoPreview({
|
||||
client,
|
||||
state,
|
||||
roomId,
|
||||
roomIdOrAlias,
|
||||
microphoneMuted,
|
||||
localVideoMuted,
|
||||
toggleLocalVideoMuted,
|
||||
@ -93,7 +93,7 @@ export function VideoPreview({
|
||||
onPress={toggleLocalVideoMuted}
|
||||
/>
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
feedbackModalState={feedbackModalState}
|
||||
feedbackModalProps={feedbackModalProps}
|
||||
inCall={false}
|
||||
|
93
src/room/useRoomParams.ts
Normal file
93
src/room/useRoomParams.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export interface RoomParams {
|
||||
roomAlias: string | null;
|
||||
roomId: string | null;
|
||||
viaServers: string[];
|
||||
// Whether the app is running in embedded mode, and should keep the user
|
||||
// confined to the current room
|
||||
isEmbedded: boolean;
|
||||
// Whether to start a walkie-talkie call instead of a video call
|
||||
isPtt: boolean;
|
||||
// Whether to use end-to-end encryption
|
||||
e2eEnabled: boolean;
|
||||
// The user's ID (only used in Matroska mode)
|
||||
userId: string | null;
|
||||
// The display name to use for auto-registration
|
||||
displayName: string | null;
|
||||
// The device's ID (only used in Matroska mode)
|
||||
deviceId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the room parameters for the current URL.
|
||||
* @param {string} query The URL query string
|
||||
* @param {string} fragment The URL fragment string
|
||||
* @returns {RoomParams} The room parameters encoded in the URL
|
||||
*/
|
||||
export const getRoomParams = (
|
||||
query: string = window.location.search,
|
||||
fragment: string = window.location.hash
|
||||
): RoomParams => {
|
||||
const fragmentQueryStart = fragment.indexOf("?");
|
||||
const fragmentParams = new URLSearchParams(
|
||||
fragmentQueryStart === -1 ? "" : fragment.substring(fragmentQueryStart)
|
||||
);
|
||||
const queryParams = new URLSearchParams(query);
|
||||
|
||||
// Normally, room params should be encoded in the fragment so as to avoid
|
||||
// leaking them to the server. However, we also check the normal query
|
||||
// string for backwards compatibility with versions that only used that.
|
||||
const hasParam = (name: string): boolean =>
|
||||
fragmentParams.has(name) || queryParams.has(name);
|
||||
const getParam = (name: string): string | null =>
|
||||
fragmentParams.get(name) ?? queryParams.get(name);
|
||||
const getAllParams = (name: string): string[] => [
|
||||
...fragmentParams.getAll(name),
|
||||
...queryParams.getAll(name),
|
||||
];
|
||||
|
||||
// The part of the fragment before the ?
|
||||
const fragmentRoute =
|
||||
fragmentQueryStart === -1
|
||||
? fragment
|
||||
: fragment.substring(0, fragmentQueryStart);
|
||||
|
||||
return {
|
||||
roomAlias: fragmentRoute.length > 1 ? fragmentRoute : null,
|
||||
roomId: getParam("roomId"),
|
||||
viaServers: getAllParams("via"),
|
||||
isEmbedded: hasParam("embed"),
|
||||
isPtt: hasParam("ptt"),
|
||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||
userId: getParam("userId"),
|
||||
displayName: getParam("displayName"),
|
||||
deviceId: getParam("deviceId"),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to simplify use of getRoomParams.
|
||||
* @returns {RoomParams} The room parameters for the current URL
|
||||
*/
|
||||
export const useRoomParams = (): RoomParams => {
|
||||
const { hash, search } = useLocation();
|
||||
return useMemo(() => getRoomParams(search, hash), [search, hash]);
|
||||
};
|
@ -41,6 +41,12 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// matrix-widget-api has its transpiled lib/index.js as its entry point,
|
||||
// which Vite for some reason refuses to work with, so we point it to
|
||||
// src/index.ts instead
|
||||
"matrix-widget-api": "matrix-widget-api/src/index.ts",
|
||||
},
|
||||
dedupe: [
|
||||
"react",
|
||||
"react-dom",
|
||||
|
46
yarn.lock
46
yarn.lock
@ -2754,6 +2754,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
||||
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
||||
|
||||
"@types/events@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||
|
||||
"@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.1.3":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
|
||||
@ -3863,12 +3868,10 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base-x@^3.0.2:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
|
||||
integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
base-x@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
|
||||
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
|
||||
|
||||
base16@^1.0.0:
|
||||
version "1.0.0"
|
||||
@ -4105,12 +4108,12 @@ browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^
|
||||
node-releases "^2.0.5"
|
||||
update-browserslist-db "^1.0.4"
|
||||
|
||||
bs58@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
|
||||
integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
|
||||
bs58@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
|
||||
integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
|
||||
dependencies:
|
||||
base-x "^3.0.2"
|
||||
base-x "^4.0.0"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.2"
|
||||
@ -8387,24 +8390,33 @@ matrix-events-sdk@^0.0.1-beta.7:
|
||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
|
||||
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8ba2d257ae24bbed61cd7fe99af081324337161c":
|
||||
version "19.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8ba2d257ae24bbed61cd7fe99af081324337161c"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#3334c01191bcd82b5243916284c9a08d08fd9795":
|
||||
version "19.2.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3334c01191bcd82b5243916284c9a08d08fd9795"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/sdp-transform" "^2.4.5"
|
||||
another-json "^0.2.0"
|
||||
browser-request "^0.3.3"
|
||||
bs58 "^4.0.1"
|
||||
bs58 "^5.0.0"
|
||||
content-type "^1.0.4"
|
||||
loglevel "^1.7.1"
|
||||
matrix-events-sdk "^0.0.1-beta.7"
|
||||
p-retry "^4.5.0"
|
||||
matrix-widget-api "^1.0.0"
|
||||
p-retry "4"
|
||||
qs "^6.9.6"
|
||||
request "^2.88.2"
|
||||
sdp-transform "^2.14.1"
|
||||
unhomoglyph "^1.0.6"
|
||||
|
||||
matrix-widget-api@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.0.0.tgz#0cde6839cca66ad817ab12aca3490ccc8bac97d1"
|
||||
integrity sha512-cy8p/8EteRPTFIAw7Q9EgPUJc2jD19ZahMR8bMKf2NkILDcjuPMC0UWnsJyB3fSnlGw+VbGepttRpULM31zX8Q==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
|
||||
md5.js@^1.3.4:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
|
||||
@ -9189,7 +9201,7 @@ p-map@^4.0.0:
|
||||
dependencies:
|
||||
aggregate-error "^3.0.0"
|
||||
|
||||
p-retry@^4.5.0:
|
||||
p-retry@4:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
|
||||
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
|
||||
|
Loading…
Reference in New Issue
Block a user