mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Human readable URLs
This tries to implement the new scheme for element call URLs, including: * All rooms have an alias * Legacy unencrypted rooms keep their old alias * New encerypted rooms get an autogenerated alias so they have a string that can always be used to join them. * The room name is hyphenated and included in the URL However this is buggy as it stands. Firstly, I am gettign initialisation weirdness where it keeps thinking I'm unregistered, then remembering all my rooms again when I enter a display name... *realises* oh, this is because it's looking for config.json in the current directory, ie. /c/. Another bug is that it doesn't update the URL bar with the canonical URL for the room, which it probably ought to. The main blocker, however, is that it turns out we use the absence of a canonical alias to tell whether a room is supposed to be an e2ee call room, so this would need that mechanism replaced.
This commit is contained in:
@@ -21,15 +21,16 @@ import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/
|
||||
|
||||
import { Modal } from "../Modal";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { getAbsoluteRoomUrl } from "../matrix-utils";
|
||||
import styles from "./AppSelectionModal.module.css";
|
||||
import { editFragmentQuery } from "../UrlParams";
|
||||
|
||||
interface Props {
|
||||
roomId: string | null;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
}
|
||||
|
||||
export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
export const AppSelectionModal: FC<Props> = ({ roomId, roomAlias }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
@@ -46,9 +47,12 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
const appUrl = useMemo(() => {
|
||||
// If the room ID is not known, fall back to the URL of the current page
|
||||
const url = new URL(
|
||||
roomId === null
|
||||
? window.location.href
|
||||
: getRoomUrl(roomId, roomSharedKey ?? undefined)
|
||||
getAbsoluteRoomUrl(
|
||||
roomId,
|
||||
undefined,
|
||||
roomAlias,
|
||||
roomSharedKey ?? undefined
|
||||
)
|
||||
);
|
||||
// Edit the URL so that it opens in embedded mode. We do this for two
|
||||
// reasons: It causes the mobile app to limit the user to only visiting the
|
||||
@@ -62,7 +66,7 @@ export const AppSelectionModal: FC<Props> = ({ roomId }) => {
|
||||
const result = new URL("element://call");
|
||||
result.searchParams.set("url", url.toString());
|
||||
return result.toString();
|
||||
}, [roomId, roomSharedKey]);
|
||||
}, [roomId, roomAlias, roomSharedKey]);
|
||||
|
||||
return (
|
||||
<Modal className={styles.modal} title={t("Select app")} open={open}>
|
||||
|
||||
@@ -14,10 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useCallback } from "react";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { MatrixError } from "matrix-js-sdk";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Link } from "@vector-im/compound-web";
|
||||
|
||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
@@ -38,6 +41,9 @@ export function GroupCallLoader({
|
||||
const { t } = useTranslation();
|
||||
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
|
||||
|
||||
const history = useHistory();
|
||||
const onHomeClick = useCallback(() => history.push("/"), [history]);
|
||||
|
||||
switch (groupCallState.kind) {
|
||||
case "loading":
|
||||
return (
|
||||
@@ -48,6 +54,24 @@ export function GroupCallLoader({
|
||||
case "loaded":
|
||||
return <>{children(groupCallState.rtcSession)}</>;
|
||||
case "failed":
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
if ((groupCallState.error as MatrixError).errcode === "M_NOT_FOUND") {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Call not found")}</h1>
|
||||
<p>
|
||||
{t(
|
||||
"Element Calls are now end-to-end encrypted and need to be explicitly created. This helps make sure everyone's using the same encryption key."
|
||||
)}
|
||||
</p>
|
||||
{/* XXX: A 'create it for me' button would be the obvious UX here. Two screens already have
|
||||
dupes of this flow, let's make a common component and put it here. */}
|
||||
<Link href="/" onClick={onHomeClick}>
|
||||
{t("Home")}
|
||||
</Link>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ export function GroupCallView({
|
||||
|
||||
const shareModal = (
|
||||
<ShareModal
|
||||
roomId={rtcSession.room.roomId}
|
||||
room={rtcSession.room}
|
||||
open={shareModalOpen}
|
||||
onDismiss={onDismissShareModal}
|
||||
/>
|
||||
|
||||
@@ -110,7 +110,10 @@ export const RoomPage: FC = () => {
|
||||
{/* On mobile, show a prompt to launch the mobile app. If in embedded mode,
|
||||
that means we *are* in the mobile app and should show no further prompt. */}
|
||||
{(platform === "android" || platform === "ios") && !isEmbedded && (
|
||||
<AppSelectionModal roomId={roomId} />
|
||||
<AppSelectionModal
|
||||
roomId={roomId ?? undefined}
|
||||
roomAlias={roomAlias ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,29 +16,30 @@ limitations under the License.
|
||||
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Room } from "matrix-js-sdk";
|
||||
|
||||
import { Modal } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { getAbsoluteRoomUrlForRoom } from "../matrix-utils";
|
||||
import styles from "./ShareModal.module.css";
|
||||
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
interface Props {
|
||||
roomId: string;
|
||||
room: Room;
|
||||
open: boolean;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const ShareModal: FC<Props> = ({ roomId, open, onDismiss }) => {
|
||||
export const ShareModal: FC<Props> = ({ room, open, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
const roomSharedKey = useRoomSharedKey(roomId);
|
||||
const roomSharedKey = useRoomSharedKey(room.roomId) ?? undefined;
|
||||
|
||||
return (
|
||||
<Modal title={t("Share this call")} open={open} onDismiss={onDismiss}>
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
|
||||
value={getAbsoluteRoomUrlForRoom(room, roomSharedKey)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -20,14 +20,11 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { setLocalStorageItem } from "../useLocalStorage";
|
||||
import { isLocalRoomId, createRoom, roomNameFromRoomId } from "../matrix-utils";
|
||||
import { useEnableE2EE } from "../settings/useSetting";
|
||||
import { getRoomSharedKeyLocalStorageKey } from "../e2ee/sharedKeyManagement";
|
||||
|
||||
export type GroupCallLoaded = {
|
||||
kind: "loaded";
|
||||
@@ -63,60 +60,46 @@ export const useLoadGroupCall = (
|
||||
|
||||
const [e2eeEnabled] = useEnableE2EE();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
try {
|
||||
let room: Room | null = null;
|
||||
if (roomIdOrAlias[0] === "#") {
|
||||
// We lowercase the localpart when we create the room, so we must lowercase
|
||||
// it here too (we just do the whole alias). We can't do the same to room IDs
|
||||
// though.
|
||||
const sanitisedIdOrAlias =
|
||||
roomIdOrAlias[0] === "#"
|
||||
? roomIdOrAlias.toLowerCase()
|
||||
: roomIdOrAlias;
|
||||
|
||||
const room = await client.joinRoom(sanitisedIdOrAlias, {
|
||||
viaServers,
|
||||
});
|
||||
logger.info(
|
||||
`Joined ${sanitisedIdOrAlias}, waiting room to be ready for group calls`
|
||||
// Also, we explicitly look up the room alias here. We previously just tried to
|
||||
// join anyway but the js-sdk recreates the room if you pass the alias for a
|
||||
// room you're already joined to (which it probably ought not to).
|
||||
const lookupResult = await client.getRoomIdForAlias(
|
||||
roomIdOrAlias.toLowerCase()
|
||||
);
|
||||
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
|
||||
logger.info(`${sanitisedIdOrAlias}, is ready for group calls`);
|
||||
return room;
|
||||
} catch (error) {
|
||||
if (
|
||||
isLocalRoomId(roomIdOrAlias, client) &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(error.errcode === "M_NOT_FOUND" ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(error.message &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
error.message.indexOf("Failed to fetch alias") !== -1))
|
||||
) {
|
||||
// The room doesn't exist, but we can create it
|
||||
const [, roomId] = await createRoom(
|
||||
client,
|
||||
roomNameFromRoomId(roomIdOrAlias),
|
||||
e2eeEnabled ?? false
|
||||
);
|
||||
|
||||
if (e2eeEnabled) {
|
||||
setLocalStorageItem(
|
||||
getRoomSharedKeyLocalStorageKey(roomId),
|
||||
randomString(32)
|
||||
);
|
||||
}
|
||||
|
||||
// likewise, wait for the room
|
||||
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||
return client.getRoom(roomId)!;
|
||||
logger.info(`${roomIdOrAlias} resolved to ${lookupResult.room_id}`);
|
||||
room = client.getRoom(lookupResult.room_id);
|
||||
if (!room) {
|
||||
logger.info(`Room ${lookupResult.room_id} not found, joining.`);
|
||||
room = await client.joinRoom(lookupResult.room_id, {
|
||||
viaServers: lookupResult.servers,
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
logger.info(
|
||||
`Already in room ${lookupResult.room_id}, not rejoining.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// room IDs we just try to join by their ID, which will not work in the
|
||||
// general case without providing some servers to join via. We could provide
|
||||
// our own server, but in practice that is implicit.
|
||||
room = await client.joinRoom(roomIdOrAlias);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Joined ${roomIdOrAlias}, waiting room to be ready for group calls`
|
||||
);
|
||||
await client.waitUntilRoomReadyForGroupCalls(room.roomId);
|
||||
logger.info(`${roomIdOrAlias}, is ready for group calls`);
|
||||
return room;
|
||||
};
|
||||
|
||||
const fetchOrCreateGroupCall = async (): Promise<MatrixRTCSession> => {
|
||||
@@ -149,7 +132,7 @@ export const useLoadGroupCall = (
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}, [client, roomIdOrAlias, viaServers, t, e2eeEnabled]);
|
||||
}, [client, roomIdOrAlias, viaServers, t, e2eeEnabled, history]);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user