Merge pull request #416 from robintown/matroska

Matroska mode
This commit is contained in:
Robin 2022-08-09 10:01:05 -04:00 committed by GitHub
commit 1dfffce606
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 337 additions and 140 deletions

View file

@ -38,7 +38,8 @@
"classnames": "^2.3.1", "classnames": "^2.3.1",
"color-hash": "^2.0.1", "color-hash": "^2.0.1",
"events": "^3.3.0", "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", "mermaid": "^8.13.8",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pako": "^2.0.4", "pako": "^2.0.4",

View file

@ -26,9 +26,14 @@ import React, {
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger";
import { ErrorView } from "./FullScreenView"; import { ErrorView } from "./FullScreenView";
import { initClient, defaultHomeserver } from "./matrix-utils"; import {
initClient,
initMatroskaClient,
defaultHomeserver,
} from "./matrix-utils";
declare global { declare global {
interface Window { interface Window {
@ -91,9 +96,23 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}); });
useEffect(() => { useEffect(() => {
const restore = async (): Promise< const init = async (): Promise<
Pick<ClientProviderState, "client" | "isPasswordlessUser"> Pick<ClientProviderState, "client" | "isPasswordlessUser">
> => { > => {
const query = new URLSearchParams(window.location.search);
const widgetId = query.get("widgetId");
const parentUrl = query.get("parentUrl");
if (widgetId && parentUrl) {
// We're inside a widget, so let's engage *Matroska mode*
logger.log("Using a Matroska client");
return {
client: await initMatroskaClient(widgetId, parentUrl),
isPasswordlessUser: false,
};
} else {
// We're running as a standalone application
try { try {
const session = loadSession(); const session = loadSession();
@ -102,6 +121,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
const { user_id, device_id, access_token, passwordlessUser } = const { user_id, device_id, access_token, passwordlessUser } =
session; session;
logger.log("Using a standalone client");
const client = await initClient( const client = await initClient(
{ {
baseUrl: defaultHomeserver, baseUrl: defaultHomeserver,
@ -118,13 +138,13 @@ export const ClientProvider: FC<Props> = ({ children }) => {
return { client: undefined, isPasswordlessUser: false }; return { client: undefined, isPasswordlessUser: false };
} catch (err) { } catch (err) {
console.error(err);
clearSession(); clearSession();
throw err; throw err;
} }
}
}; };
restore() init()
.then(({ client, isPasswordlessUser }) => { .then(({ client, isPasswordlessUser }) => {
setState({ setState({
client, client,
@ -135,7 +155,8 @@ export const ClientProvider: FC<Props> = ({ children }) => {
error: undefined, error: undefined,
}); });
}) })
.catch(() => { .catch((err) => {
logger.error(err);
setState({ setState({
client: undefined, client: undefined,
loading: false, loading: false,

View file

@ -37,7 +37,7 @@ import { AriaDialogProps } from "@react-types/dialog";
import { ReactComponent as CloseIcon } from "./icons/Close.svg"; import { ReactComponent as CloseIcon } from "./icons/Close.svg";
import styles from "./Modal.module.css"; import styles from "./Modal.module.css";
interface ModalProps extends OverlayProps, AriaDialogProps { export interface ModalProps extends OverlayProps, AriaDialogProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
className?: string; className?: string;

View file

@ -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 { 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, 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 { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
import { ClientEvent } from "matrix-js-sdk/src/client"; 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 { 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 { WidgetApi } from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import IndexedDBWorker from "./IndexedDBWorker?worker"; import IndexedDBWorker from "./IndexedDBWorker?worker";
import { getRoomParams } from "./room/useRoomParams";
export const defaultHomeserver = export const defaultHomeserver =
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ?? (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 * If true is passed for the 'restore' parameter, a check will be made
* to ensure that corresponding crypto data is stored and recovered. * to ensure that corresponding crypto data is stored and recovered.
* If the check fails, CryptoStoreIntegrityError will be thrown. * If the check fails, CryptoStoreIntegrityError will be thrown.
@ -127,16 +201,13 @@ export async function initClient(
storeOpts.cryptoStore = new MemoryCryptoStore(); 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 // it would be much better to read them in one place and pass
// the values around, but we initialise the matrix client in // the values around, but we initialise the matrix client in
// many different places so we'd have to pass it into all of // many different places so we'd have to pass it into all of
// them. // them.
const params = new URLSearchParams(window.location.search); const { e2eEnabled } = getRoomParams();
// disable e2e only if enableE2e=false is given if (!e2eEnabled) {
const enableE2e = params.get("enableE2e") !== "false";
if (!enableE2e) {
logger.info("Disabling E2E: group call signalling will NOT be encrypted."); logger.info("Disabling E2E: group call signalling will NOT be encrypted.");
} }
@ -144,10 +215,10 @@ export async function initClient(
...storeOpts, ...storeOpts,
...clientOptions, ...clientOptions,
useAuthorizationHeader: true, 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. // so we don't want API calls taking ages, we'd rather they just fail.
localTimeoutMs: 5000, localTimeoutMs: 5000,
useE2eForGroupCall: enableE2e, useE2eForGroupCall: e2eEnabled,
}); });
try { try {
@ -254,17 +325,17 @@ export async function createRoom(
return [fullAliasFromRoomName(name, client), result.room_id]; return [fullAliasFromRoomName(name, client), result.room_id];
} }
export function getRoomUrl(roomId: string): string { export function getRoomUrl(roomIdOrAlias: string): string {
if (roomId.startsWith("#")) { if (roomIdOrAlias.startsWith("#")) {
const [localPart, host] = roomId.replace("#", "").split(":"); const [localPart, host] = roomIdOrAlias.replace("#", "").split(":");
if (host !== defaultHomeserverHost) { if (host !== defaultHomeserverHost) {
return `${window.location.protocol}//${window.location.host}/room/${roomId}`; return `${window.location.protocol}//${window.location.host}/room/${roomIdOrAlias}`;
} else { } else {
return `${window.location.protocol}//${window.location.host}/${localPart}`; return `${window.location.protocol}//${window.location.host}/${localPart}`;
} }
} else { } else {
return `${window.location.protocol}//${window.location.host}/room/${roomId}`; return `${window.location.protocol}//${window.location.host}/room/#?roomId=${roomIdOrAlias}`;
} }
} }

View file

@ -23,7 +23,7 @@ import { usePageTitle } from "../usePageTitle";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
roomId: string; roomIdOrAlias: string;
viaServers: string[]; viaServers: string[];
children: (groupCall: GroupCall) => ReactNode; children: (groupCall: GroupCall) => ReactNode;
createPtt: boolean; createPtt: boolean;
@ -31,14 +31,14 @@ interface Props {
export function GroupCallLoader({ export function GroupCallLoader({
client, client,
roomId, roomIdOrAlias,
viaServers, viaServers,
children, children,
createPtt, createPtt,
}: Props): JSX.Element { }: Props): JSX.Element {
const { loading, error, groupCall } = useLoadGroupCall( const { loading, error, groupCall } = useLoadGroupCall(
client, client,
roomId, roomIdOrAlias,
viaServers, viaServers,
createPtt createPtt
); );

View file

@ -37,14 +37,14 @@ interface Props {
client: MatrixClient; client: MatrixClient;
isPasswordlessUser: boolean; isPasswordlessUser: boolean;
isEmbedded: boolean; isEmbedded: boolean;
roomId: string; roomIdOrAlias: string;
groupCall: GroupCall; groupCall: GroupCall;
} }
export function GroupCallView({ export function GroupCallView({
client, client,
isPasswordlessUser, isPasswordlessUser,
isEmbedded, isEmbedded,
roomId, roomIdOrAlias,
groupCall, groupCall,
}: Props) { }: Props) {
const { const {
@ -101,7 +101,7 @@ export function GroupCallView({
return ( return (
<PTTCallView <PTTCallView
client={client} client={client}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
roomName={groupCall.room.name} roomName={groupCall.room.name}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
groupCall={groupCall} groupCall={groupCall}
@ -129,7 +129,7 @@ export function GroupCallView({
isScreensharing={isScreensharing} isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed} localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds} screenshareFeeds={screenshareFeeds}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers} unencryptedEventsFromUsers={unencryptedEventsFromUsers}
/> />
); );
@ -164,7 +164,7 @@ export function GroupCallView({
localVideoMuted={localVideoMuted} localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted} toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted} toggleMicrophoneMuted={toggleMicrophoneMuted}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
isEmbedded={isEmbedded} isEmbedded={isEmbedded}
/> />
); );

View file

@ -71,7 +71,7 @@ interface Props {
isScreensharing: boolean; isScreensharing: boolean;
screenshareFeeds: CallFeed[]; screenshareFeeds: CallFeed[];
localScreenshareFeed: CallFeed; localScreenshareFeed: CallFeed;
roomId: string; roomIdOrAlias: string;
unencryptedEventsFromUsers: Set<string>; unencryptedEventsFromUsers: Set<string>;
} }
@ -99,7 +99,7 @@ export function InCallView({
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localScreenshareFeed, localScreenshareFeed,
roomId, roomIdOrAlias,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
}: Props) { }: Props) {
usePreventScroll(); usePreventScroll();
@ -260,7 +260,7 @@ export function InCallView({
{!fullscreenParticipant && ( {!fullscreenParticipant && (
<OverflowMenu <OverflowMenu
inCall inCall
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall} groupCall={groupCall}
showInvite={true} showInvite={true}
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}
@ -277,7 +277,7 @@ export function InCallView({
{rageshakeRequestModalState.isOpen && ( {rageshakeRequestModalState.isOpen && (
<RageshakeRequestModal <RageshakeRequestModal
{...rageshakeRequestModalProps} {...rageshakeRequestModalProps}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
/> />
)} )}
</div> </div>

View file

@ -14,21 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils"; import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css"; import styles from "./InviteModal.module.css";
export function InviteModal({ interface Props extends Omit<ModalProps, "title" | "children"> {
roomId, roomIdOrAlias: string;
...rest }
}: {
roomId: string; export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
[x: string]: unknown;
}) {
return (
<Modal <Modal
title="Invite People" title="Invite People"
isDismissable isDismissable
@ -37,8 +34,10 @@ export function InviteModal({
> >
<ModalContent> <ModalContent>
<p>Copy and share this meeting link</p> <p>Copy and share this meeting link</p>
<CopyButton className={styles.copyButton} value={getRoomUrl(roomId)} /> <CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
}

View file

@ -45,7 +45,7 @@ interface Props {
toggleLocalVideoMuted: () => void; toggleLocalVideoMuted: () => void;
toggleMicrophoneMuted: () => void; toggleMicrophoneMuted: () => void;
localVideoMuted: boolean; localVideoMuted: boolean;
roomId: string; roomIdOrAlias: string;
isEmbedded: boolean; isEmbedded: boolean;
} }
export function LobbyView({ export function LobbyView({
@ -61,7 +61,7 @@ export function LobbyView({
localVideoMuted, localVideoMuted,
toggleLocalVideoMuted, toggleLocalVideoMuted,
toggleMicrophoneMuted, toggleMicrophoneMuted,
roomId, roomIdOrAlias,
isEmbedded, isEmbedded,
}: Props) { }: Props) {
const { stream } = useCallFeed(localCallFeed); const { stream } = useCallFeed(localCallFeed);
@ -115,7 +115,7 @@ export function LobbyView({
<VideoPreview <VideoPreview
state={state} state={state}
client={client} client={client}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
microphoneMuted={microphoneMuted} microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted} localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted} toggleLocalVideoMuted={toggleLocalVideoMuted}
@ -136,7 +136,7 @@ export function LobbyView({
<Body>Or</Body> <Body>Or</Body>
<CopyButton <CopyButton
variant="secondaryCopy" variant="secondaryCopy"
value={getRoomUrl(roomId)} value={getRoomUrl(roomIdOrAlias)}
className={styles.copyButton} className={styles.copyButton}
copiedMessage="Call link copied" copiedMessage="Call link copied"
> >

View file

@ -32,7 +32,7 @@ import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip"; import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal"; import { FeedbackModal } from "./FeedbackModal";
interface Props { interface Props {
roomId: string; roomIdOrAlias: string;
inCall: boolean; inCall: boolean;
groupCall: GroupCall; groupCall: GroupCall;
showInvite: boolean; showInvite: boolean;
@ -43,7 +43,7 @@ interface Props {
}; };
} }
export function OverflowMenu({ export function OverflowMenu({
roomId, roomIdOrAlias,
inCall, inCall,
groupCall, groupCall,
showInvite, showInvite,
@ -119,7 +119,7 @@ export function OverflowMenu({
</PopoverMenuTrigger> </PopoverMenuTrigger>
{settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />} {settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />}
{inviteModalState.isOpen && ( {inviteModalState.isOpen && (
<InviteModal roomId={roomId} {...inviteModalProps} /> <InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)} )}
{feedbackModalState.isOpen && ( {feedbackModalState.isOpen && (
<FeedbackModal <FeedbackModal

View file

@ -87,7 +87,7 @@ function getPromptText(
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
roomId: string; roomIdOrAlias: string;
roomName: string; roomName: string;
avatarUrl: string; avatarUrl: string;
groupCall: GroupCall; groupCall: GroupCall;
@ -99,7 +99,7 @@ interface Props {
export const PTTCallView: React.FC<Props> = ({ export const PTTCallView: React.FC<Props> = ({
client, client,
roomId, roomIdOrAlias,
roomName, roomName,
avatarUrl, avatarUrl,
groupCall, groupCall,
@ -205,7 +205,7 @@ export const PTTCallView: React.FC<Props> = ({
<div className={styles.footer}> <div className={styles.footer}>
<OverflowMenu <OverflowMenu
inCall inCall
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall} groupCall={groupCall}
showInvite={false} showInvite={false}
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}
@ -282,7 +282,7 @@ export const PTTCallView: React.FC<Props> = ({
</div> </div>
{inviteModalState.isOpen && showControls && ( {inviteModalState.isOpen && showControls && (
<InviteModal roomId={roomId} {...inviteModalProps} /> <InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)} )}
</div> </div>
); );

View file

@ -14,24 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input"; import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake"; import { useSubmitRageshake } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
export function RageshakeRequestModal({ interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId,
roomId,
...rest
}: {
rageshakeRequestId: string; rageshakeRequestId: string;
roomId: string; roomIdOrAlias: string;
onClose: () => void; onClose: () => void;
[x: string]: unknown; }
}) {
export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId,
roomIdOrAlias,
...rest
}) => {
const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => { useEffect(() => {
@ -53,7 +54,7 @@ export function RageshakeRequestModal({
submitRageshake({ submitRageshake({
sendLogs: true, sendLogs: true,
rageshakeRequestId, rageshakeRequestId,
roomId, roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
}) })
} }
disabled={sending} disabled={sending}
@ -69,4 +70,4 @@ export function RageshakeRequestModal({
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
} };

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import React, { useEffect, useMemo, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView"; import { GroupCallView } from "./GroupCallView";
import { useRoomParams } from "./useRoomParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler"; import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
export function RoomPage() { export const RoomPage: FC = () => {
const { loading, isAuthenticated, error, client, isPasswordlessUser } = const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient(); useClient();
const { roomId: maybeRoomId } = useParams(); const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } =
const { hash, search }: { hash: string; search: string } = useLocation(); useRoomParams();
const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => { const roomIdOrAlias = roomId ?? roomAlias;
const params = new URLSearchParams(search); if (!roomIdOrAlias) throw new Error("No room specified");
return [
params.getAll("via"),
params.has("embed"),
params.get("ptt") === "true",
params.get("displayName"),
];
}, [search]);
const roomId = (maybeRoomId || hash || "").toLowerCase();
const { registerPasswordlessUser } = useRegisterPasswordlessUser(); const { registerPasswordlessUser } = useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false); const [isRegistering, setIsRegistering] = useState(false);
@ -76,14 +69,14 @@ export function RoomPage() {
<MediaHandlerProvider client={client}> <MediaHandlerProvider client={client}>
<GroupCallLoader <GroupCallLoader
client={client} client={client}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers} viaServers={viaServers}
createPtt={isPtt} createPtt={isPtt}
> >
{(groupCall) => ( {(groupCall) => (
<GroupCallView <GroupCallView
client={client} client={client}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
groupCall={groupCall} groupCall={groupCall}
isPasswordlessUser={isPasswordlessUser} isPasswordlessUser={isPasswordlessUser}
isEmbedded={isEmbedded} isEmbedded={isEmbedded}
@ -92,4 +85,4 @@ export function RoomPage() {
</GroupCallLoader> </GroupCallLoader>
</MediaHandlerProvider> </MediaHandlerProvider>
); );
} };

View file

@ -32,7 +32,7 @@ import { useModalTriggerState } from "../Modal";
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
state: GroupCallState; state: GroupCallState;
roomId: string; roomIdOrAlias: string;
microphoneMuted: boolean; microphoneMuted: boolean;
localVideoMuted: boolean; localVideoMuted: boolean;
toggleLocalVideoMuted: () => void; toggleLocalVideoMuted: () => void;
@ -43,7 +43,7 @@ interface Props {
export function VideoPreview({ export function VideoPreview({
client, client,
state, state,
roomId, roomIdOrAlias,
microphoneMuted, microphoneMuted,
localVideoMuted, localVideoMuted,
toggleLocalVideoMuted, toggleLocalVideoMuted,
@ -93,7 +93,7 @@ export function VideoPreview({
onPress={toggleLocalVideoMuted} onPress={toggleLocalVideoMuted}
/> />
<OverflowMenu <OverflowMenu
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps} feedbackModalProps={feedbackModalProps}
inCall={false} inCall={false}

93
src/room/useRoomParams.ts Normal file
View 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]);
};

View file

@ -41,6 +41,12 @@ export default defineConfig(({ mode }) => {
}, },
}, },
resolve: { 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: [ dedupe: [
"react", "react",
"react-dom", "react-dom",

View file

@ -2754,6 +2754,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== 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": "@types/glob@*", "@types/glob@^7.1.1", "@types/glob@^7.1.3":
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" 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" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-x@^3.0.2: base-x@^4.0.0:
version "3.0.9" version "4.0.0"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
dependencies:
safe-buffer "^5.0.1"
base16@^1.0.0: base16@^1.0.0:
version "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" node-releases "^2.0.5"
update-browserslist-db "^1.0.4" update-browserslist-db "^1.0.4"
bs58@^4.0.1: bs58@^5.0.0:
version "4.0.1" version "5.0.0"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
dependencies: dependencies:
base-x "^3.0.2" base-x "^4.0.0"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.2" 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" resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8ba2d257ae24bbed61cd7fe99af081324337161c": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#3334c01191bcd82b5243916284c9a08d08fd9795":
version "19.0.0" version "19.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8ba2d257ae24bbed61cd7fe99af081324337161c" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3334c01191bcd82b5243916284c9a08d08fd9795"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
"@types/sdp-transform" "^2.4.5" "@types/sdp-transform" "^2.4.5"
another-json "^0.2.0" another-json "^0.2.0"
browser-request "^0.3.3" browser-request "^0.3.3"
bs58 "^4.0.1" bs58 "^5.0.0"
content-type "^1.0.4" content-type "^1.0.4"
loglevel "^1.7.1" loglevel "^1.7.1"
matrix-events-sdk "^0.0.1-beta.7" 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" qs "^6.9.6"
request "^2.88.2" request "^2.88.2"
sdp-transform "^2.14.1" sdp-transform "^2.14.1"
unhomoglyph "^1.0.6" 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: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -9189,7 +9201,7 @@ p-map@^4.0.0:
dependencies: dependencies:
aggregate-error "^3.0.0" aggregate-error "^3.0.0"
p-retry@^4.5.0: p-retry@4:
version "4.6.2" version "4.6.2"
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==