Merge branch 'livekit' into resize-observer

This commit is contained in:
Robin
2024-09-03 15:35:10 -04:00
134 changed files with 2547 additions and 4147 deletions

View File

@@ -22,7 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { Modal } from "../Modal";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
import { getAbsoluteRoomUrl } from "../matrix-utils";
import { getAbsoluteRoomUrl } from "../utils/matrix";
import styles from "./AppSelectionModal.module.css";
import { editFragmentQuery } from "../UrlParams";
import { E2eeType } from "../e2ee/e2eeType";

View File

@@ -42,9 +42,8 @@ limitations under the License.
}
.callEndedButton {
margin: auto;
margin-top: 54px;
margin-left: 30px;
margin-right: 30px !important;
}
.submitButton {

View File

@@ -18,17 +18,19 @@ import { FC, FormEventHandler, ReactNode, useCallback, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { Button } from "@vector-im/compound-web";
import styles from "./CallEndedView.module.css";
import feedbackStyle from "../input/FeedbackInput.module.css";
import { Button, LinkButton } from "../button";
import { useProfile } from "../profile/useProfile";
import { Body, Link, Headline } from "../typography/Typography";
import { Body, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { FieldRow, InputField } from "../input/Input";
import { StarRatingInput } from "../input/StarRatingInput";
import { RageshakeButton } from "../settings/RageshakeButton";
import { Link } from "../button/Link";
import { LinkButton } from "../button";
interface Props {
client: MatrixClient;
@@ -95,12 +97,7 @@ export const CallEndedView: FC<Props> = ({
calls
</p>
</Trans>
<LinkButton
className={styles.callEndedButton}
size="lg"
variant="default"
to="/register"
>
<LinkButton className={styles.callEndedButton} to="/register">
{t("call_ended_view.create_account_button")}
</LinkButton>
</div>
@@ -136,8 +133,6 @@ export const CallEndedView: FC<Props> = ({
<Button
type="submit"
className={styles.submitButton}
size="lg"
variant="default"
data-testid="home_go"
>
{submitting ? t("submitting") : t("action.submit")}
@@ -159,7 +154,7 @@ export const CallEndedView: FC<Props> = ({
</Trans>
</Headline>
<div className={styles.disconnectedButtons}>
<Button size="lg" variant="default" onClick={reconnect}>
<Button onClick={reconnect}>
{t("call_ended_view.reconnect_button")}
</Button>
<div className={styles.rageshakeButton}>
@@ -169,9 +164,7 @@ export const CallEndedView: FC<Props> = ({
</main>
{!confineToRoom && (
<Body className={styles.footer}>
<Link color="primary" to="/">
{t("return_home_button")}
</Link>
<Link to="/"> {t("return_home_button")} </Link>
</Body>
)}
</>
@@ -198,9 +191,7 @@ export const CallEndedView: FC<Props> = ({
</main>
{!confineToRoom && (
<Body className={styles.footer}>
<Link color="primary" to="/">
{t("call_ended_view.not_now_button")}
</Link>
<Link to="/"> {t("call_ended_view.not_now_button")} </Link>
</Body>
)}
</>

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { useCallback } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { MatrixError } from "matrix-js-sdk";
import { MatrixError } from "matrix-js-sdk/src/matrix";
import { useHistory } from "react-router-dom";
import { Heading, Link, Text } from "@vector-im/compound-web";

View File

@@ -35,7 +35,7 @@ import { MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { findDeviceByName } from "../media-utils";
import { findDeviceByName } from "../utils/media";
import { ActiveCall } from "./InCallView";
import { MUTE_PARTICIPANT_COUNT, MuteStates } from "./MuteStates";
import { useMediaDevices, MediaDevices } from "../livekit/MediaDevicesContext";

View File

@@ -58,6 +58,10 @@ limitations under the License.
);
}
.footer.hidden {
display: none;
}
.footer.overlay {
position: absolute;
inset-block-end: 0;
@@ -67,6 +71,7 @@ limitations under the License.
}
.footer.overlay.hidden {
display: grid;
opacity: 0;
pointer-events: none;
}

View File

@@ -19,7 +19,6 @@ import {
RoomContext,
useLocalParticipant,
} from "@livekit/components-react";
import { usePreventScroll } from "@react-aria/overlays";
import { ConnectionState, Room } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {
@@ -44,10 +43,10 @@ import LogoMark from "../icons/LogoMark.svg?react";
import LogoType from "../icons/LogoType.svg?react";
import type { IWidgetApiRequest } from "matrix-widget-api";
import {
HangupButton,
EndCallButton,
MicButton,
VideoButton,
ScreenshareButton,
ShareScreenButton,
SettingsButton,
} from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
@@ -69,7 +68,7 @@ import { InviteButton } from "../button/InviteButton";
import { LayoutToggle } from "./LayoutToggle";
import { ECConnectionState } from "../livekit/useECConnectionState";
import { useOpenIDSFU } from "../livekit/openIDSFU";
import { GridMode, Layout, useCallViewModel } from "../state/CallViewModel";
import { CallViewModel, GridMode, Layout } from "../state/CallViewModel";
import { Grid, TileProps } from "../grid/Grid";
import { useObservable } from "../state/useObservable";
import { useInitial } from "../useInitial";
@@ -93,7 +92,7 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
const maxTapDurationMs = 400;
export interface ActiveCallProps
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
e2eeSystem: EncryptionSystem;
}
@@ -105,6 +104,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
sfuConfig,
props.e2eeSystem,
);
const connStateObservable = useObservable(connState);
const [vm, setVm] = useState<CallViewModel | null>(null);
useEffect(() => {
return (): void => {
@@ -113,17 +114,41 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!livekitRoom) return null;
useEffect(() => {
if (livekitRoom !== undefined) {
const vm = new CallViewModel(
props.rtcSession.room,
livekitRoom,
props.e2eeSystem.kind !== E2eeType.NONE,
connStateObservable,
);
setVm(vm);
return (): void => vm.destroy();
}
}, [
props.rtcSession.room,
livekitRoom,
props.e2eeSystem.kind,
connStateObservable,
]);
if (livekitRoom === undefined || vm === null) return null;
return (
<RoomContext.Provider value={livekitRoom}>
<InCallView {...props} livekitRoom={livekitRoom} connState={connState} />
<InCallView
{...props}
vm={vm}
livekitRoom={livekitRoom}
connState={connState}
/>
</RoomContext.Provider>
);
};
export interface InCallViewProps {
client: MatrixClient;
vm: CallViewModel;
matrixInfo: MatrixInfo;
rtcSession: MatrixRTCSession;
livekitRoom: Room;
@@ -138,6 +163,7 @@ export interface InCallViewProps {
export const InCallView: FC<InCallViewProps> = ({
client,
vm,
matrixInfo,
rtcSession,
livekitRoom,
@@ -148,7 +174,6 @@ export const InCallView: FC<InCallViewProps> = ({
connState,
onShareClick,
}) => {
usePreventScroll();
useWakeLock();
useEffect(() => {
@@ -193,12 +218,6 @@ export const InCallView: FC<InCallViewProps> = ({
const reducedControls = boundsValid && bounds.width <= 340;
const noControls = reducedControls && bounds.height <= 400;
const vm = useCallViewModel(
rtcSession.room,
livekitRoom,
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
connState,
);
const windowMode = useObservableEagerState(vm.windowMode);
const layout = useObservableEagerState(vm.layout);
const gridMode = useObservableEagerState(vm.gridMode);
@@ -471,14 +490,14 @@ export const InCallView: FC<InCallViewProps> = ({
<MicButton
key="1"
muted={!muteStates.audio.enabled}
onPress={toggleMicrophone}
onClick={toggleMicrophone}
disabled={muteStates.audio.setEnabled === null}
data-testid="incall_mute"
/>,
<VideoButton
key="2"
muted={!muteStates.video.enabled}
onPress={toggleCamera}
onClick={toggleCamera}
disabled={muteStates.video.setEnabled === null}
data-testid="incall_videomute"
/>,
@@ -486,21 +505,21 @@ export const InCallView: FC<InCallViewProps> = ({
if (!reducedControls) {
if (canScreenshare && !hideScreensharing) {
buttons.push(
<ScreenshareButton
<ShareScreenButton
key="3"
enabled={isScreenShareEnabled}
onPress={toggleScreensharing}
onClick={toggleScreensharing}
data-testid="incall_screenshare"
/>,
);
}
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
buttons.push(<SettingsButton key="4" onClick={openSettings} />);
}
buttons.push(
<HangupButton
<EndCallButton
key="6"
onPress={function (): void {
onClick={function (): void {
onLeave();
}}
data-testid="incall_leave"

View File

@@ -24,3 +24,12 @@ limitations under the License.
.button {
width: 100%;
}
.qrCode {
display: flex;
justify-content: center;
}
.qrCode img {
margin-block-end: var(--cpd-space-8x);
}

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { FC, MouseEvent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Room } from "matrix-js-sdk";
import { Room } from "matrix-js-sdk/src/matrix";
import { Button, Text } from "@vector-im/compound-web";
import {
LinkIcon,
@@ -25,10 +25,11 @@ import {
import useClipboard from "react-use-clipboard";
import { Modal } from "../Modal";
import { getAbsoluteRoomUrl } from "../matrix-utils";
import { getAbsoluteRoomUrl } from "../utils/matrix";
import styles from "./InviteModal.module.css";
import { Toast } from "../Toast";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
import { QrCode } from "../QrCode";
interface Props {
room: Room;
@@ -61,6 +62,7 @@ export const InviteModal: FC<Props> = ({ room, open, onDismiss }) => {
return (
<>
<Modal title={t("invite_modal.title")} open={open} onDismiss={onDismiss}>
<QrCode className={styles.qrCode} data={url} />
<Text className={styles.url} size="sm" weight="semibold">
{url}
</Text>

View File

@@ -19,7 +19,6 @@ limitations under the License.
border: 1px solid var(--cpd-color-border-interactive-secondary);
border-radius: var(--cpd-radius-pill-effect);
background: var(--cpd-color-bg-canvas-default);
box-shadow: 0px 0px 40px 0px rgba(0, 0, 0, 0.5);
display: flex;
position: relative;
}
@@ -32,9 +31,9 @@ limitations under the License.
inline-size: var(--cpd-space-11x);
cursor: pointer;
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-primary);
background: var(--cpd-color-bg-action-secondary-rest);
box-shadow: var(--small-drop-shadow);
transition: background-color 0.1s;
}
.toggle svg {
@@ -43,6 +42,7 @@ limitations under the License.
padding: calc(2.5 * var(--cpd-space-1x));
pointer-events: none;
color: var(--cpd-color-icon-primary);
transition: color 0.1s;
}
.toggle svg:nth-child(2) {
@@ -61,7 +61,7 @@ limitations under the License.
}
.toggle input:active {
background: var(--cpd-color-bg-action-secondary-hovered);
background: var(--cpd-color-bg-action-secondary-pressed);
box-shadow: none;
}
@@ -80,7 +80,7 @@ limitations under the License.
}
.toggle input:checked:active {
background: var(--cpd-color-bg-action-primary-hovered);
background: var(--cpd-color-bg-action-primary-pressed);
}
.toggle input:first-child {

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { FC, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Button, Link } from "@vector-im/compound-web";
import { Button } from "@vector-im/compound-web";
import classNames from "classnames";
import { useHistory } from "react-router-dom";
@@ -29,7 +29,7 @@ import { MatrixInfo, VideoPreview } from "./VideoPreview";
import { MuteStates } from "./MuteStates";
import { InviteButton } from "../button/InviteButton";
import {
HangupButton,
EndCallButton,
MicButton,
SettingsButton,
VideoButton,
@@ -37,6 +37,7 @@ import {
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
import { useMediaQuery } from "../useMediaQuery";
import { E2eeType } from "../e2ee/e2eeType";
import { Link } from "../button/Link";
interface Props {
client: MatrixClient;
@@ -92,7 +93,7 @@ export const LobbyView: FC<Props> = ({
const recentsButtonInFooter = useMediaQuery("(max-height: 500px)");
const recentsButton = !confineToRoom && (
<Link className={styles.recents} href="#" onClick={onLeaveClick}>
<Link className={styles.recents} to="/">
{t("lobby.leave_button")}
</Link>
);
@@ -140,16 +141,16 @@ export const LobbyView: FC<Props> = ({
<div className={inCallStyles.buttons}>
<MicButton
muted={!muteStates.audio.enabled}
onPress={onAudioPress}
onClick={onAudioPress}
disabled={muteStates.audio.setEnabled === null}
/>
<VideoButton
muted={!muteStates.video.enabled}
onPress={onVideoPress}
onClick={onVideoPress}
disabled={muteStates.video.setEnabled === null}
/>
<SettingsButton onPress={openSettings} />
{!confineToRoom && <HangupButton onPress={onLeaveClick} />}
<SettingsButton onClick={openSettings} />
{!confineToRoom && <EndCallButton onClick={onLeaveClick} />}
</div>
</div>
</div>

View File

@@ -16,9 +16,9 @@ limitations under the License.
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@vector-im/compound-web";
import { Modal, Props as ModalProps } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
@@ -52,7 +52,7 @@ export const RageshakeRequestModal: FC<Props> = ({
<Body>{t("rageshake_request_modal.body")}</Body>
<FieldRow>
<Button
onPress={(): void =>
onClick={(): void =>
void submitRageshake({
sendLogs: true,
rageshakeRequestId,

View File

@@ -18,9 +18,9 @@ import { FC, useCallback, useState } from "react";
import { useLocation } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
import { Button } from "@vector-im/compound-web";
import styles from "./RoomAuthView.module.css";
import { Button } from "../button";
import { Body, Caption, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
@@ -117,7 +117,7 @@ export const RoomAuthView: FC = () => {
Not registered yet?{" "}
<Link
color="primary"
to={{ pathname: "/login", state: { from: location } }}
to={{ pathname: "/register", state: { from: location } }}
>
Create an account
</Link>

View File

@@ -35,10 +35,7 @@ import { LobbyView } from "./LobbyView";
import { E2eeType } from "../e2ee/e2eeType";
import { useProfile } from "../profile/useProfile";
import { useMuteStates } from "./MuteStates";
import {
useSetting,
optInAnalytics as optInAnalyticsSetting,
} from "../settings/settings";
import { useOptInAnalytics } from "../settings/settings";
export const RoomPage: FC = () => {
const {
@@ -83,7 +80,7 @@ export const RoomPage: FC = () => {
registerPasswordlessUser,
]);
const [optInAnalytics, setOptInAnalytics] = useSetting(optInAnalyticsSetting);
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
useEffect(() => {
// During the beta, opt into analytics by default
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);

View File

@@ -0,0 +1,164 @@
/*
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 { vi, Mocked, test, expect } from "vitest";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
import { withFakeTimers } from "../utils/test";
const withMockedPosthog = (
continuation: (posthog: Mocked<PosthogAnalytics>) => void,
): void => {
const posthog = vi.mocked({
trackEvent: vi.fn(),
} as unknown as PosthogAnalytics);
const instanceSpy = vi
.spyOn(PosthogAnalytics, "instance", "get")
.mockReturnValue(posthog);
try {
continuation(posthog);
} finally {
instanceSpy.mockRestore();
}
};
const mockRoomState = (
groupCallMemberContents: Record<string, unknown>[],
): RoomState => {
const stateEvents = groupCallMemberContents.map((content) => ({
getContent: (): Record<string, unknown> => content,
}));
return { getStateEvents: () => stateEvents } as unknown as RoomState;
};
test("checkForParallelCalls does nothing if all participants are in the same call", () => {
withFakeTimers(() => {
withMockedPosthog((posthog) => {
const roomState = mockRoomState([
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Call",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
{
"m.call_id": null, // invalid
"m.devices": [
{
device_id: "Element Android",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
null, // invalid
],
},
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Desktop",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
],
},
]);
checkForParallelCalls(roomState);
expect(posthog.trackEvent).not.toHaveBeenCalled();
});
});
});
test("checkForParallelCalls sends diagnostics to PostHog if there is a split-brain", () => {
withFakeTimers(() => {
withMockedPosthog((posthog) => {
const roomState = mockRoomState([
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Call",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
{
"m.call_id": "2",
"m.devices": [
{
device_id: "Element Android",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
],
},
{
"m.calls": [
{
"m.call_id": "1",
"m.devices": [
{
device_id: "Element Desktop",
session_id: "a",
expires_ts: Date.now() + 1000,
},
],
},
{
"m.call_id": "2",
"m.devices": [
{
device_id: "Element Call",
session_id: "a",
expires_ts: Date.now() - 1000,
},
],
},
],
},
]);
checkForParallelCalls(roomState);
expect(posthog.trackEvent).toHaveBeenCalledWith({
eventName: "ParallelCalls",
participantsPerCall: {
"1": 2,
"2": 1,
},
});
});
});
});

View File

@@ -26,7 +26,7 @@ import { SyncState } from "matrix-js-sdk/src/sync";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { RoomEvent, Room } from "matrix-js-sdk/src/models/room";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { JoinRule } from "matrix-js-sdk";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import { useTranslation } from "react-i18next";
import { widget } from "../widget";

View File

@@ -1,64 +0,0 @@
/*
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 { useEffect } from "react";
import { platform } from "../Platform";
export function usePageUnload(callback: () => void): void {
useEffect(() => {
let pageVisibilityTimeout: ReturnType<typeof setTimeout>;
function onBeforeUnload(event: PageTransitionEvent): void {
if (event.type === "visibilitychange") {
if (document.visibilityState === "visible") {
clearTimeout(pageVisibilityTimeout);
} else {
// Wait 5 seconds before closing the page to avoid accidentally leaving
// TODO: Make this configurable?
pageVisibilityTimeout = setTimeout(() => {
callback();
}, 5000);
}
} else {
callback();
}
}
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
if (platform === "ios") {
window.addEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
document.addEventListener("visibilitychange", onBeforeUnload);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.addEventListener("beforeunload", onBeforeUnload);
return (): void => {
window.removeEventListener("pagehide", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
document.removeEventListener("visibilitychange", onBeforeUnload);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.removeEventListener("beforeunload", onBeforeUnload);
clearTimeout(pageVisibilityTimeout);
};
}, [callback]);
}