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 { ErrorView } from "./FullScreenView";
import { initClient, initMatroskaClient, defaultHomeserver } from "./matrix-utils";
import {
initClient,
initMatroskaClient,
defaultHomeserver,
} from "./matrix-utils";
declare global {
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 { 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, 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 { ClientEvent } from "matrix-js-sdk/src/client";
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 IndexedDBWorker from "./IndexedDBWorker?worker";
import { getRoomParams } from "./room/useRoomParams";
export const defaultHomeserver =
(import.meta.env.VITE_DEFAULT_HOMESERVER as string) ??
@ -82,20 +87,20 @@ const SEND_RECV_TO_DEVICE = [
* @returns The MatrixClient instance
*/
export async function initMatroskaClient(
widgetId: string, parentUrl: string,
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
// The rest of the data we need is encoded in the fragment so as to avoid
// leaking it to the server
const fragmentQueryStart = window.location.hash.indexOf("?");
const roomId = window.location.hash.substring(0, fragmentQueryStart);
const fragmentQuery = new URLSearchParams(window.location.hash.substring(fragmentQueryStart));
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");
// 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!
// We don't even need to set up crypto
const client = createRoomWidgetClient(
new WidgetApi(widgetId, new URL(parentUrl).origin),
{
@ -103,14 +108,15 @@ export async function initMatroskaClient(
receiveState: SEND_RECV_STATE,
sendToDevice: SEND_RECV_TO_DEVICE,
receiveToDevice: SEND_RECV_TO_DEVICE,
turnServers: true,
},
roomId,
{
baseUrl: "",
userId: fragmentQuery.get("userId"),
deviceId: fragmentQuery.get("deviceId"),
userId,
deviceId,
timelineSupport: true,
},
}
);
await client.startClient();
@ -192,16 +198,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.");
}
@ -212,7 +215,7 @@ export async function initClient(
// 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 {
@ -319,17 +322,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}`;
}
}

View file

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

View file

@ -31,7 +31,7 @@ export function GroupCallView({
client,
isPasswordlessUser,
isEmbedded,
roomId,
roomIdOrAlias,
groupCall,
}) {
const {
@ -89,7 +89,7 @@ export function GroupCallView({
return (
<PTTCallView
client={client}
roomId={roomId}
roomIdOrAlias={roomIdOrAlias}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
groupCall={groupCall}
@ -117,7 +117,7 @@ export function GroupCallView({
isScreensharing={isScreensharing}
localScreenshareFeed={localScreenshareFeed}
screenshareFeeds={screenshareFeeds}
roomId={roomId}
roomIdOrAlias={roomIdOrAlias}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
/>
);
@ -153,7 +153,7 @@ export function GroupCallView({
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
toggleMicrophoneMuted={toggleMicrophoneMuted}
roomId={roomId}
roomIdOrAlias={roomIdOrAlias}
isEmbedded={isEmbedded}
/>
);

View file

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

View file

@ -20,7 +20,7 @@ import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
export function InviteModal({ roomId, ...rest }) {
export function InviteModal({ roomIdOrAlias, ...rest }) {
return (
<Modal
title="Invite People"
@ -30,7 +30,10 @@ export function InviteModal({ roomId, ...rest }) {
>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton className={styles.copyButton} value={getRoomUrl(roomId)} />
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);

View file

@ -41,7 +41,7 @@ export function LobbyView({
localVideoMuted,
toggleLocalVideoMuted,
toggleMicrophoneMuted,
roomId,
roomIdOrAlias,
isEmbedded,
}) {
const { stream } = useCallFeed(localCallFeed);
@ -95,7 +95,7 @@ export function LobbyView({
<VideoPreview
state={state}
client={client}
roomId={roomId}
roomIdOrAlias={roomIdOrAlias}
microphoneMuted={microphoneMuted}
localVideoMuted={localVideoMuted}
toggleLocalVideoMuted={toggleLocalVideoMuted}
@ -116,7 +116,7 @@ export function LobbyView({
<Body>Or</Body>
<CopyButton
variant="secondaryCopy"
value={getRoomUrl(roomId)}
value={getRoomUrl(roomIdOrAlias)}
className={styles.copyButton}
copiedMessage="Call link copied"
>

View file

@ -30,7 +30,7 @@ import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
export function OverflowMenu({
roomId,
roomIdOrAlias,
inCall,
groupCall,
showInvite,
@ -88,7 +88,7 @@ export function OverflowMenu({
</PopoverMenuTrigger>
{settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />}
{inviteModalState.isOpen && (
<InviteModal roomId={roomId} {...inviteModalProps} />
<InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} />
)}
{feedbackModalState.isOpen && (
<FeedbackModal

View file

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

View file

@ -21,7 +21,11 @@ import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
export function RageshakeRequestModal({
rageshakeRequestId,
roomIdOrAlias,
...rest
}) {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
@ -43,7 +47,7 @@ export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId,
roomIdOrAlias, // Possibly not a room ID, but oh well
})
}
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");
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.
*/
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";
@ -28,20 +28,12 @@ export function RoomPage() {
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
const { roomId: maybeRoomId } = useParams();
const { hash, search } = 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 { registerPasswordlessUser, recaptchaId } =
useRegisterPasswordlessUser();
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);
useEffect(() => {
@ -76,14 +68,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}

View file

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