Skip to content

Commit

Permalink
Merge branch 'livekit' into hughns/type-check-built-config
Browse files Browse the repository at this point in the history
  • Loading branch information
hughns committed Dec 18, 2024
2 parents 7783c72 + 6d5dc0d commit f09aad2
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 42 deletions.
57 changes: 29 additions & 28 deletions docs/url-params.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"version": "{{productName}} version: {{version}}",
"video_tile": {
"always_show": "Always show",
"camera_starting": "Video loading...",
"change_fit_contain": "Fit to frame",
"collapse": "Collapse",
"expand": "Expand",
Expand Down
2 changes: 1 addition & 1 deletion src/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const sizes = new Map([
[Size.XL, 90],
]);

interface Props {
export interface Props {
id: string;
name: string;
className?: string;
Expand Down
50 changes: 49 additions & 1 deletion src/UrlParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details.

import { describe, expect, it } from "vitest";

import { getRoomIdentifierFromUrl, getUrlParams } from "../src/UrlParams";
import {
getRoomIdentifierFromUrl,
getUrlParams,
UserIntent,
} from "../src/UrlParams";

const ROOM_NAME = "roomNameHere";
const ROOM_ID = "!d45f138fsd";
Expand Down Expand Up @@ -195,4 +199,48 @@ describe("UrlParams", () => {
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
});
});

describe("intent", () => {
it("defaults to unknown", () => {
expect(getUrlParams().intent).toBe(UserIntent.Unknown);
});

it("ignores intent if it is not a valid value", () => {
expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown);
});

it("accepts start_call", () => {
expect(getUrlParams("?intent=start_call").intent).toBe(
UserIntent.StartNewCall,
);
});

it("accepts join_existing", () => {
expect(getUrlParams("?intent=join_existing").intent).toBe(
UserIntent.JoinExistingCall,
);
});
});

describe("skipLobby", () => {
it("defaults to false", () => {
expect(getUrlParams().skipLobby).toBe(false);
});

it("defaults to false if intent is start_call in SPA mode", () => {
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
});

it("defaults to true if intent is start_call in widget mode", () => {
expect(
getUrlParams(
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
).skipLobby,
).toBe(true);
});

it("default to false if intent is join_existing", () => {
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
});
});
});
23 changes: 22 additions & 1 deletion src/UrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ interface RoomIdentifier {
viaServers: string[];
}

export enum UserIntent {
StartNewCall = "start_call",
JoinExistingCall = "join_existing",
Unknown = "unknown",
}

// If you need to add a new flag to this interface, prefer a name that describes
// a specific behavior (such as 'confineToRoom'), rather than one that describes
// the situations that call for this behavior ('isEmbedded'). This makes it
Expand Down Expand Up @@ -142,6 +148,13 @@ export interface UrlParams {
* creating a spa link.
*/
homeserver: string | null;

/**
* The user's intent with respect to the call.
* e.g. if they clicked a Start Call button, this would be `start_call`.
* If it was a Join Call button, it would be `join_existing`.
*/
intent: string | null;
}

// This is here as a stopgap, but what would be far nicer is a function that
Expand Down Expand Up @@ -211,6 +224,10 @@ export const getUrlParams = (

const fontScale = parseFloat(parser.getParam("fontScale") ?? "");

let intent = parser.getParam("intent");
if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) {
intent = UserIntent.Unknown;
}
const widgetId = parser.getParam("widgetId");
const parentUrl = parser.getParam("parentUrl");
const isWidget = !!widgetId && !!parentUrl;
Expand Down Expand Up @@ -243,11 +260,15 @@ export const getUrlParams = (
analyticsID: parser.getParam("analyticsID"),
allowIceFallback: parser.getFlagParam("allowIceFallback"),
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
skipLobby: parser.getFlagParam("skipLobby"),
skipLobby: parser.getFlagParam(
"skipLobby",
isWidget && intent === UserIntent.StartNewCall,
),
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true,
theme: parser.getParam("theme"),
viaServers: !isWidget ? parser.getParam("viaServers") : null,
homeserver: !isWidget ? parser.getParam("homeserver") : null,
intent,
};
};

Expand Down
11 changes: 11 additions & 0 deletions src/room/VideoPreview.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ video.mirror {
transform: scaleX(-1);
}

.preview .cameraStarting {
position: absolute;
top: var(--cpd-space-10x);
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
color: var(--cpd-color-text-secondary);
}

.avatarContainer {
position: absolute;
top: 0;
Expand Down
73 changes: 73 additions & 0 deletions src/room/VideoPreview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { expect, describe, it, vi, beforeAll } from "vitest";
import { render } from "@testing-library/react";

import { type MatrixInfo, VideoPreview } from "./VideoPreview";
import { type MuteStates } from "./MuteStates";
import { E2eeType } from "../e2ee/e2eeType";

function mockMuteStates({ audio = true, video = true } = {}): MuteStates {
return {
audio: { enabled: audio, setEnabled: vi.fn() },
video: { enabled: video, setEnabled: vi.fn() },
};
}

describe("VideoPreview", () => {
const matrixInfo: MatrixInfo = {
userId: "@a:example.org",
displayName: "Alice",
avatarUrl: "",
roomId: "",
roomName: "",
e2eeSystem: { kind: E2eeType.NONE },
roomAlias: null,
roomAvatar: null,
};

beforeAll(() => {
window.ResizeObserver = class ResizeObserver {
public observe(): void {
// do nothing
}
public unobserve(): void {
// do nothing
}
public disconnect(): void {
// do nothing
}
};
});

it("shows avatar with video disabled", () => {
const { queryByRole } = render(
<VideoPreview
matrixInfo={matrixInfo}
muteStates={mockMuteStates({ video: false })}
videoTrack={null}
children={<></>}
/>,
);
expect(queryByRole("img", { name: "@a:example.org" })).toBeVisible();
});

it("shows loading status with video enabled but no track", () => {
const { queryByRole } = render(
<VideoPreview
matrixInfo={matrixInfo}
muteStates={mockMuteStates({ video: true })}
videoTrack={null}
children={<></>}
/>,
);
expect(queryByRole("status")).toHaveTextContent(
"video_tile.camera_starting",
);
});
});
37 changes: 26 additions & 11 deletions src/room/VideoPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { useEffect, useRef, type FC, type ReactNode } from "react";
import { useEffect, useMemo, useRef, type FC, type ReactNode } from "react";
import useMeasure from "react-use-measure";
import { facingModeFromLocalTrack, type LocalVideoTrack } from "livekit-client";
import classNames from "classnames";
import { useTranslation } from "react-i18next";

import { Avatar } from "../Avatar";
import { TileAvatar } from "../tile/TileAvatar";
import styles from "./VideoPreview.module.css";
import { type MuteStates } from "./MuteStates";
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
Expand Down Expand Up @@ -39,6 +40,7 @@ export const VideoPreview: FC<Props> = ({
videoTrack,
children,
}) => {
const { t } = useTranslation();
const [previewRef, previewBounds] = useMeasure();

const videoEl = useRef<HTMLVideoElement | null>(null);
Expand All @@ -53,6 +55,11 @@ export const VideoPreview: FC<Props> = ({
};
}, [videoTrack]);

const cameraIsStarting = useMemo(
() => muteStates.video.enabled && !videoTrack,
[muteStates.video.enabled, videoTrack],
);

return (
<div className={classNames(styles.preview)} ref={previewRef}>
<video
Expand All @@ -69,15 +76,23 @@ export const VideoPreview: FC<Props> = ({
tabIndex={-1}
disablePictureInPicture
/>
{!muteStates.video.enabled && (
<div className={styles.avatarContainer}>
<Avatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={Math.min(previewBounds.width, previewBounds.height) / 2}
src={matrixInfo.avatarUrl}
/>
</div>
{(!muteStates.video.enabled || cameraIsStarting) && (
<>
<div className={styles.avatarContainer}>
{cameraIsStarting && (
<div className={styles.cameraStarting} role="status">
{t("video_tile.camera_starting")}
</div>
)}
<TileAvatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={Math.min(previewBounds.width, previewBounds.height) / 2}
src={matrixInfo.avatarUrl}
loading={cameraIsStarting}
/>
</div>
</>
)}
<div className={styles.buttonBar}>{children}</div>
</div>
Expand Down
20 changes: 20 additions & 0 deletions src/tile/TileAvatar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

.loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.5;
/* TODO: make this --cpd-color-fg-primary when available. */
color: var(--cpd-color-text-primary);
}
27 changes: 27 additions & 0 deletions src/tile/TileAvatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { expect, describe, it } from "vitest";
import { render } from "@testing-library/react";

import { TileAvatar } from "./TileAvatar";

describe("TileAvatar", () => {
it("should show loading spinner when loading", () => {
const { container } = render(
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={true} />,
);
expect(container.querySelector(".loading")).toBeInTheDocument();
});

it("should not show loading spinner when not loading", () => {
const { container } = render(
<TileAvatar id="@a:example.org" name="Alice" size={96} loading={false} />,
);
expect(container.querySelector(".loading")).not.toBeInTheDocument();
});
});
30 changes: 30 additions & 0 deletions src/tile/TileAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { type FC } from "react";
import { InlineSpinner } from "@vector-im/compound-web";

import styles from "./TileAvatar.module.css";
import { Avatar, type Props as AvatarProps } from "../Avatar";

interface Props extends AvatarProps {
size: number;
loading?: boolean;
}

export const TileAvatar: FC<Props> = ({ size, loading, ...props }) => {
return (
<div>
{loading && (
<div className={styles.loading}>
<InlineSpinner size={size / 3} />
</div>
)}
<Avatar size={size} {...props} />
</div>
);
};
7 changes: 7 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,12 @@ export default defineConfig(({ mode }) => {
"@radix-ui/react-dismissable-layer",
],
},
// Vite is using esbuild in development mode, which doesn't work with the wasm loader
// in matrix-sdk-crypto-wasm, so we need to exclude it here. This doesn't affect the
// production build (which uses rollup) which still works as expected.
// https://vite.dev/guide/why.html#why-not-bundle-with-esbuild
optimizeDeps: {
exclude: ["@matrix-org/matrix-sdk-crypto-wasm"],
},
};
});

0 comments on commit f09aad2

Please sign in to comment.