Add a URL param for room ID

And consolidate our URL params logic
This commit is contained in:
Robin Townsend 2022-07-27 16:14:05 -04:00
parent 2a8cb3c4e2
commit cf56b24dda
13 changed files with 166 additions and 67 deletions

View file

@ -29,7 +29,11 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { ErrorView } from "./FullScreenView"; import { ErrorView } from "./FullScreenView";
import { initClient, initMatroskaClient, defaultHomeserver } from "./matrix-utils"; import {
initClient,
initMatroskaClient,
defaultHomeserver,
} from "./matrix-utils";
declare global { declare global {
interface Window { interface Window {

View file

@ -5,7 +5,11 @@ 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, createRoomWidgetClient, 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 { EventType } from "matrix-js-sdk/src/@types/event";
@ -15,6 +19,7 @@ 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) ??
@ -82,20 +87,20 @@ const SEND_RECV_TO_DEVICE = [
* @returns The MatrixClient instance * @returns The MatrixClient instance
*/ */
export async function initMatroskaClient( export async function initMatroskaClient(
widgetId: string, parentUrl: string, widgetId: string,
parentUrl: string
): Promise<MatrixClient> { ): Promise<MatrixClient> {
// In this mode, we use a special client which routes all requests through // In this mode, we use a special client which routes all requests through
// the host application via the widget API // the host application via the widget API
// The rest of the data we need is encoded in the fragment so as to avoid const { roomId, userId, deviceId } = getRoomParams();
// leaking it to the server if (!roomId) throw new Error("Room ID must be supplied");
const fragmentQueryStart = window.location.hash.indexOf("?"); if (!userId) throw new Error("User ID must be supplied");
const roomId = window.location.hash.substring(0, fragmentQueryStart); if (!deviceId) throw new Error("Device ID must be supplied");
const fragmentQuery = new URLSearchParams(window.location.hash.substring(fragmentQueryStart));
// Since all data should be coming from the host application, there's no // 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 // need to persist anything, and therefore we can use the default stores
// We don't even need to set up crypto! // We don't even need to set up crypto
const client = createRoomWidgetClient( const client = createRoomWidgetClient(
new WidgetApi(widgetId, new URL(parentUrl).origin), new WidgetApi(widgetId, new URL(parentUrl).origin),
{ {
@ -103,14 +108,15 @@ export async function initMatroskaClient(
receiveState: SEND_RECV_STATE, receiveState: SEND_RECV_STATE,
sendToDevice: SEND_RECV_TO_DEVICE, sendToDevice: SEND_RECV_TO_DEVICE,
receiveToDevice: SEND_RECV_TO_DEVICE, receiveToDevice: SEND_RECV_TO_DEVICE,
turnServers: true,
}, },
roomId, roomId,
{ {
baseUrl: "", baseUrl: "",
userId: fragmentQuery.get("userId"), userId,
deviceId: fragmentQuery.get("deviceId"), deviceId,
timelineSupport: true, timelineSupport: true,
}, }
); );
await client.startClient(); await client.startClient();
@ -192,16 +198,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.");
} }
@ -212,7 +215,7 @@ export async function initClient(
// Use a relatively low timeout for API calls: this is a realtime app // 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 {
@ -319,17 +322,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

@ -21,14 +21,14 @@ import { usePageTitle } from "../usePageTitle";
export function GroupCallLoader({ export function GroupCallLoader({
client, client,
roomId, roomIdOrAlias,
viaServers, viaServers,
createPtt, createPtt,
children, children,
}) { }) {
const { loading, error, groupCall } = useLoadGroupCall( const { loading, error, groupCall } = useLoadGroupCall(
client, client,
roomId, roomIdOrAlias,
viaServers, viaServers,
createPtt createPtt
); );

View file

@ -31,7 +31,7 @@ export function GroupCallView({
client, client,
isPasswordlessUser, isPasswordlessUser,
isEmbedded, isEmbedded,
roomId, roomIdOrAlias,
groupCall, groupCall,
}) { }) {
const { const {
@ -89,7 +89,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}
@ -117,7 +117,7 @@ export function GroupCallView({
isScreensharing={isScreensharing} isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed} localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds} screenshareFeeds={screenshareFeeds}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers} unencryptedEventsFromUsers={unencryptedEventsFromUsers}
/> />
); );
@ -153,7 +153,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

@ -65,7 +65,7 @@ export function InCallView({
toggleScreensharing, toggleScreensharing,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
roomId, roomIdOrAlias,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
}) { }) {
usePreventScroll(); usePreventScroll();
@ -184,7 +184,7 @@ export function InCallView({
)} )}
<OverflowMenu <OverflowMenu
inCall inCall
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
showInvite={true} showInvite={true}
@ -201,7 +201,7 @@ export function InCallView({
{rageshakeRequestModalState.isOpen && ( {rageshakeRequestModalState.isOpen && (
<RageshakeRequestModal <RageshakeRequestModal
{...rageshakeRequestModalProps} {...rageshakeRequestModalProps}
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
/> />
)} )}
</div> </div>

View file

@ -20,7 +20,7 @@ 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({ roomId, ...rest }) { export function InviteModal({ roomIdOrAlias, ...rest }) {
return ( return (
<Modal <Modal
title="Invite People" title="Invite People"
@ -30,7 +30,10 @@ export function InviteModal({ roomId, ...rest }) {
> >
<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

@ -41,7 +41,7 @@ export function LobbyView({
localVideoMuted, localVideoMuted,
toggleLocalVideoMuted, toggleLocalVideoMuted,
toggleMicrophoneMuted, toggleMicrophoneMuted,
roomId, roomIdOrAlias,
isEmbedded, isEmbedded,
}) { }) {
const { stream } = useCallFeed(localCallFeed); const { stream } = useCallFeed(localCallFeed);
@ -95,7 +95,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}
@ -116,7 +116,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

@ -30,7 +30,7 @@ import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal"; import { FeedbackModal } from "./FeedbackModal";
export function OverflowMenu({ export function OverflowMenu({
roomId, roomIdOrAlias,
inCall, inCall,
groupCall, groupCall,
showInvite, showInvite,
@ -88,7 +88,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

@ -86,7 +86,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;
@ -98,7 +98,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,
@ -204,7 +204,7 @@ export const PTTCallView: React.FC<Props> = ({
<div className={styles.footer}> <div className={styles.footer}>
<OverflowMenu <OverflowMenu
inCall inCall
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
showInvite={false} showInvite={false}
@ -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

@ -21,7 +21,11 @@ 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({ rageshakeRequestId, roomId, ...rest }) { export function RageshakeRequestModal({
rageshakeRequestId,
roomIdOrAlias,
...rest
}) {
const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => { useEffect(() => {
@ -43,7 +47,7 @@ export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
submitRageshake({ submitRageshake({
sendLogs: true, sendLogs: true,
rageshakeRequestId, rageshakeRequestId,
roomId, roomIdOrAlias, // Possibly not a room ID, but oh well
}) })
} }
disabled={sending} disabled={sending}

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,13 +14,13 @@ 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";
@ -28,20 +28,12 @@ export function RoomPage() {
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 } = 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"), const { registerPasswordlessUser } = useRegisterPasswordlessUser();
params.has("embed"),
params.get("ptt") === "true",
params.get("displayName"),
];
}, [search]);
const roomId = (maybeRoomId || hash || "").toLowerCase();
const { registerPasswordlessUser, recaptchaId } =
useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false); const [isRegistering, setIsRegistering] = useState(false);
useEffect(() => { useEffect(() => {
@ -76,14 +68,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}

View file

@ -30,7 +30,7 @@ import { useModalTriggerState } from "../Modal";
export function VideoPreview({ export function VideoPreview({
client, client,
state, state,
roomId, roomIdOrAlias,
microphoneMuted, microphoneMuted,
localVideoMuted, localVideoMuted,
toggleLocalVideoMuted, toggleLocalVideoMuted,
@ -80,7 +80,7 @@ export function VideoPreview({
onPress={toggleLocalVideoMuted} onPress={toggleLocalVideoMuted}
/> />
<OverflowMenu <OverflowMenu
roomId={roomId} roomIdOrAlias={roomIdOrAlias}
client={client} client={client}
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps} feedbackModalProps={feedbackModalProps}

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]);
};