Merge remote-tracking branch 'origin/livekit' into dbkr/refactor_urlparams

This commit is contained in:
David Baker
2023-09-18 17:54:49 +01:00
39 changed files with 1792 additions and 1879 deletions

View File

@@ -0,0 +1,33 @@
/*
Copyright 2023 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.
*/
.modal p {
text-align: center;
margin-block-end: var(--cpd-space-8x);
}
.modal button,
.modal a {
width: 100%;
}
.modal button {
margin-block-end: var(--cpd-space-6x);
}
.modal a {
box-sizing: border-box;
}

View File

@@ -0,0 +1,80 @@
/*
Copyright 2023 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 { FC, MouseEvent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Text } from "@vector-im/compound-web";
import { ReactComponent as PopOutIcon } from "@vector-im/compound-design-tokens/icons/pop-out.svg";
import { Modal } from "../Modal";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
import { getRoomUrl } from "../matrix-utils";
import styles from "./AppSelectionModal.module.css";
import { editFragmentQuery } from "../UrlParams";
interface Props {
roomId: string | null;
}
export const AppSelectionModal: FC<Props> = ({ roomId }) => {
const { t } = useTranslation();
const [open, setOpen] = useState(true);
const onBrowserClick = useCallback(
(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setOpen(false);
},
[setOpen]
);
const roomSharedKey = useRoomSharedKey(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)
);
// 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
// room in question, and it prevents this app selection prompt from being
// shown a second time.
url.hash = editFragmentQuery(url.hash, (params) => {
params.set("isEmbedded", "");
return params;
});
const result = new URL("element://call");
result.searchParams.set("url", url.toString());
return result.toString();
}, [roomId, roomSharedKey]);
return (
<Modal className={styles.modal} title={t("Select app")} open={open}>
<Text size="md" weight="semibold">
{t("Ready to join?")}
</Text>
<Button kind="secondary" onClick={onBrowserClick}>
{t("Continue in browser")}
</Button>
<Button as="a" href={appUrl} Icon={PopOutIcon}>
{t("Open in the app")}
</Button>
</Modal>
);
};

View File

@@ -17,7 +17,8 @@ limitations under the License.
.headline {
text-align: center;
margin-bottom: 60px;
white-space: pre;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.callEndedContent {
@@ -66,6 +67,7 @@ limitations under the License.
flex: 1;
flex-direction: column;
align-items: center;
padding-inline: var(--inline-content-inset);
}
.logo {

View File

@@ -27,7 +27,6 @@ interface Props {
roomIdOrAlias: string;
viaServers: string[];
children: (rtcSession: MatrixRTCSession) => ReactNode;
createPtt: boolean;
}
export function GroupCallLoader({
@@ -35,15 +34,9 @@ export function GroupCallLoader({
roomIdOrAlias,
viaServers,
children,
createPtt,
}: Props): JSX.Element {
const { t } = useTranslation();
const groupCallState = useLoadGroupCall(
client,
roomIdOrAlias,
viaServers,
createPtt
);
const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers);
switch (groupCallState.kind) {
case "loading":

View File

@@ -45,7 +45,6 @@ import {
import { useEnableE2EE } from "../settings/useSetting";
import { useRoomAvatar } from "./useRoomAvatar";
import { useRoomName } from "./useRoomName";
import { useModalTriggerState } from "../Modal";
import { useJoinRule } from "./useJoinRule";
import { ShareModal } from "./ShareModal";
@@ -286,12 +285,15 @@ export function GroupCallView({
const joinRule = useJoinRule(rtcSession.room);
const { modalState: shareModalState, modalProps: shareModalProps } =
useModalTriggerState();
const [shareModalOpen, setShareModalOpen] = useState(false);
const onDismissShareModal = useCallback(
() => setShareModalOpen(false),
[setShareModalOpen]
);
const onShareClickFn = useCallback(
() => shareModalState.open(),
[shareModalState]
() => setShareModalOpen(true),
[setShareModalOpen]
);
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
@@ -311,8 +313,12 @@ export function GroupCallView({
return <ErrorView error={new Error("You need to enable E2EE to join.")} />;
}
const shareModal = shareModalState.isOpen && (
<ShareModal roomId={rtcSession.room.roomId} {...shareModalProps} />
const shareModal = (
<ShareModal
roomId={rtcSession.room.roomId}
open={shareModalOpen}
onDismiss={onDismissShareModal}
/>
);
if (isJoined) {

View File

@@ -30,7 +30,6 @@ import { Room as MatrixRoom } from "matrix-js-sdk/src/models/room";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import useMeasure from "react-use-measure";
import { OverlayTriggerState } from "@react-stately/overlays";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
@@ -51,7 +50,6 @@ import {
VideoGrid,
} from "../video-grid/VideoGrid";
import { useShowConnectionStats } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
@@ -313,25 +311,20 @@ export function InCallView({
);
};
const {
modalState: rageshakeRequestModalState,
modalProps: rageshakeRequestModalProps,
} = useRageshakeRequestModal(rtcSession.room.roomId);
const rageshakeRequestModalProps = useRageshakeRequestModal(
rtcSession.room.roomId
);
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(() => {
settingsModalState.open();
}, [settingsModalState]);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const toggleScreensharing = useCallback(async () => {
exitFullscreen();
@@ -442,19 +435,13 @@ export function InCallView({
show={showInspector}
/>
)*/}
{rageshakeRequestModalState.isOpen && !noControls && (
<RageshakeRequestModal
{...rageshakeRequestModalProps}
roomId={rtcSession.room.roomId}
/>
)}
{settingsModalState.isOpen && (
<SettingsModal
client={client}
roomId={rtcSession.room.roomId}
{...settingsModalProps}
/>
)}
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
<SettingsModal
client={client}
roomId={rtcSession.room.roomId}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
</div>
);
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Modal, ModalProps } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
@@ -26,51 +26,49 @@ import { Body } from "../typography/Typography";
interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId: string;
roomId: string;
onClose: () => void;
open: boolean;
onDismiss: () => void;
}
export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId,
roomId,
...rest
open,
onDismiss,
}) => {
const { t } = useTranslation();
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
if (sent) {
rest.onClose();
}
}, [sent, rest]);
if (sent) onDismiss();
}, [sent, onDismiss]);
return (
<Modal title={t("Debug log request")} isDismissable {...rest}>
<ModalContent>
<Body>
{t(
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
)}
</Body>
<FieldRow>
<Button
onPress={() =>
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId,
})
}
disabled={sending}
>
{sending ? t("Sending debug logs…") : t("Send debug logs")}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
<Modal title={t("Debug log request")} open={open} onDismiss={onDismiss}>
<Body>
{t(
"Another user on this call is having an issue. In order to better diagnose these issues we'd like to collect a debug log."
)}
</ModalContent>
</Body>
<FieldRow>
<Button
onPress={() =>
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomId,
})
}
disabled={sending}
>
{sending ? t("Sending debug logs…") : t("Send debug logs")}
</Button>
</FieldRow>
{error && (
<FieldRow>
<ErrorMessage error={error} />
</FieldRow>
)}
</Modal>
);
};

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { FC, useEffect, useState, useCallback } from "react";
import { FC, useEffect, useState, useCallback, ReactNode } from "react";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useClientLegacy } from "../ClientContext";
@@ -26,10 +26,11 @@ import { useRoomIdentifier, useUrlParams } from "../UrlParams";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { useOptInAnalytics } from "../settings/useSetting";
import { HomePage } from "../home/HomePage";
import { platform } from "../Platform";
import { AppSelectionModal } from "./AppSelectionModal";
export const RoomPage: FC = () => {
const { isEmbedded, preload, hideHeader, isPtt, displayName } =
useUrlParams();
const { isEmbedded, preload, hideHeader, displayName } = useUrlParams();
const { roomAlias, roomId, viaServers } = useRoomIdentifier();
@@ -81,30 +82,36 @@ export const RoomPage: FC = () => {
[client, passwordlessUser, isEmbedded, preload, hideHeader]
);
let content: ReactNode;
if (loading || isRegistering) {
return <LoadingView />;
}
if (error) {
return <ErrorView error={error} />;
}
if (!client) {
return <RoomAuthView />;
}
if (!roomIdOrAlias) {
return <HomePage />;
content = <LoadingView />;
} else if (error) {
content = <ErrorView error={error} />;
} else if (!client) {
content = <RoomAuthView />;
} else if (!roomIdOrAlias) {
// TODO: This doesn't belong here, the app routes need to be reworked
content = <HomePage />;
} else {
content = (
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
>
{groupCallView}
</GroupCallLoader>
);
}
return (
<GroupCallLoader
client={client}
roomIdOrAlias={roomIdOrAlias}
viaServers={viaServers}
createPtt={isPtt}
>
{groupCallView}
</GroupCallLoader>
<>
{content}
{/* 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} />
)}
</>
);
};

View File

@@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.inviteModal {
max-width: 413px;
}
.copyButton {
width: 100%;
}

View File

@@ -17,35 +17,30 @@ limitations under the License.
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Modal } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./ShareModal.module.css";
import { useRoomSharedKey } from "../e2ee/sharedKeyManagement";
interface Props extends Omit<ModalProps, "title" | "children"> {
interface Props {
roomId: string;
open: boolean;
onDismiss: () => void;
}
export const ShareModal: FC<Props> = ({ roomId, ...rest }) => {
export const ShareModal: FC<Props> = ({ roomId, open, onDismiss }) => {
const { t } = useTranslation();
const roomSharedKey = useRoomSharedKey(roomId);
return (
<Modal
title={t("Share this call")}
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>{t("Copy and share this call link")}</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomId, roomSharedKey ?? undefined)}
data-testid="modal_inviteLink"
/>
</ModalContent>
<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)}
data-testid="modal_inviteLink"
/>
</Modal>
);
};

View File

@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect, useCallback, useMemo, useRef, FC } from "react";
import { useEffect, useCallback, useMemo, useRef, FC, useState } from "react";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { OverlayTriggerState } from "@react-stately/overlays";
import { usePreviewTracks } from "@livekit/components-react";
import {
CreateLocalTracksOptions,
@@ -28,7 +27,6 @@ import {
import { MicButton, SettingsButton, VideoButton } from "../button";
import { Avatar } from "../Avatar";
import styles from "./VideoPreview.module.css";
import { useModalTriggerState } from "../Modal";
import { SettingsModal } from "../settings/SettingsModal";
import { useClient } from "../ClientContext";
import { useMediaDevices } from "../livekit/MediaDevicesContext";
@@ -54,20 +52,16 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
const { client } = useClient();
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const openSettings = useCallback(() => {
settingsModalState.open();
}, [settingsModalState]);
const openSettings = useCallback(
() => setSettingsModalOpen(true),
[setSettingsModalOpen]
);
const closeSettings = useCallback(
() => setSettingsModalOpen(false),
[setSettingsModalOpen]
);
const devices = useMediaDevices();
@@ -153,8 +147,12 @@ export const VideoPreview: FC<Props> = ({ matrixInfo, muteStates }) => {
<SettingsButton onPress={openSettings} />
</div>
</>
{settingsModalState.isOpen && client && (
<SettingsModal client={client} {...settingsModalProps} />
{client && (
<SettingsModal
client={client}
open={settingsModalOpen}
onDismiss={closeSettings}
/>
)}
</div>
);

View File

@@ -56,8 +56,7 @@ export interface GroupCallLoadState {
export const useLoadGroupCall = (
client: MatrixClient,
roomIdOrAlias: string,
viaServers: string[],
createPtt: boolean
viaServers: string[]
): GroupCallStatus => {
const { t } = useTranslation();
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
@@ -101,7 +100,6 @@ export const useLoadGroupCall = (
const [, roomId] = await createRoom(
client,
roomNameFromRoomId(roomIdOrAlias),
createPtt,
e2eeEnabled ?? false
);
@@ -151,7 +149,7 @@ export const useLoadGroupCall = (
.then(fetchOrCreateGroupCall)
.then((rtcSession) => setState({ kind: "loaded", rtcSession }))
.catch((error) => setState({ kind: "failed", error }));
}, [client, roomIdOrAlias, viaServers, createPtt, t, e2eeEnabled]);
}, [client, roomIdOrAlias, viaServers, t, e2eeEnabled]);
return state;
};

View File

@@ -16,21 +16,7 @@ limitations under the License.
import { useEffect } from "react";
// https://stackoverflow.com/a/9039885
function isIOS() {
return (
[
"iPad Simulator",
"iPhone Simulator",
"iPod Simulator",
"iPad",
"iPhone",
"iPod",
].includes(navigator.platform) ||
// iPad on iOS 13 detection
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
}
import { platform } from "../Platform";
export function usePageUnload(callback: () => void) {
useEffect(() => {
@@ -53,7 +39,7 @@ export function usePageUnload(callback: () => void) {
}
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
if (isIOS()) {
if (platform === "ios") {
window.addEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore