mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-05 19:59:20 +00:00
Merge branch 'livekit' into robin/earpiece-overlay
This commit is contained in:
@@ -27,5 +27,5 @@ On mobile platforms (iOS, Android), web views do not reliably support selecting
|
|||||||
Callbacks for buttons in EC that are handled by the native application
|
Callbacks for buttons in EC that are handled by the native application
|
||||||
|
|
||||||
- `showNativeAudioDevicePicker: (() => void) | undefined`. Callback called whenever the user presses the output button in the settings menu.
|
- `showNativeAudioDevicePicker: (() => void) | undefined`. Callback called whenever the user presses the output button in the settings menu.
|
||||||
This button is only shown on iOS. (`userAgent.includes("iPhone")`)
|
This button is only shown on iOS. (`/iPad|iPhone|iPod|Mac/.test(navigator.userAgent)`)
|
||||||
- `onBackButtonPressed: (() => void) | undefined`. Callback when the webview detects a tab on the header's back button.
|
- `onBackButtonPressed: (() => void) | undefined`. Callback when the webview detects a tab on the header's back button.
|
||||||
|
|||||||
104
playwright/sfu-reconnect-bug.spec.ts
Normal file
104
playwright/sfu-reconnect-bug.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("When creator left, avoid reconnect to the same SFU", async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
// Use reduce motion to disable animations that are making the tests a bit flaky
|
||||||
|
const creatorContext = await browser.newContext({ reducedMotion: "reduce" });
|
||||||
|
const creatorPage = await creatorContext.newPage();
|
||||||
|
|
||||||
|
await creatorPage.goto("/");
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ARRANGE: The first user creates a call as guest, join it, then click the invite button to copy the invite link
|
||||||
|
// ========
|
||||||
|
await creatorPage.getByTestId("home_callName").click();
|
||||||
|
await creatorPage.getByTestId("home_callName").fill("Welcome");
|
||||||
|
await creatorPage.getByTestId("home_displayName").click();
|
||||||
|
await creatorPage.getByTestId("home_displayName").fill("Inviter");
|
||||||
|
await creatorPage.getByTestId("home_go").click();
|
||||||
|
await expect(creatorPage.locator("video")).toBeVisible();
|
||||||
|
|
||||||
|
// join
|
||||||
|
await creatorPage.getByTestId("lobby_joinCall").click();
|
||||||
|
// Spotlight mode to make checking the test visually clearer
|
||||||
|
await creatorPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
// Get the invite link
|
||||||
|
await creatorPage.getByRole("button", { name: "Invite" }).click();
|
||||||
|
await expect(
|
||||||
|
creatorPage.getByRole("heading", { name: "Invite to this call" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(creatorPage.getByRole("img", { name: "QR Code" })).toBeVisible();
|
||||||
|
await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible();
|
||||||
|
await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible();
|
||||||
|
await creatorPage.getByTestId("modal_inviteLink").click();
|
||||||
|
|
||||||
|
const inviteLink = (await creatorPage.evaluate(
|
||||||
|
"navigator.clipboard.readText()",
|
||||||
|
)) as string;
|
||||||
|
expect(inviteLink).toContain("room/#/");
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ACT: The other user use the invite link to join the call as a guest
|
||||||
|
// ========
|
||||||
|
const guestB = await browser.newContext({
|
||||||
|
reducedMotion: "reduce",
|
||||||
|
});
|
||||||
|
const guestBPage = await guestB.newPage();
|
||||||
|
|
||||||
|
await guestBPage.goto(inviteLink);
|
||||||
|
await guestBPage.getByTestId("joincall_displayName").fill("Invitee");
|
||||||
|
await expect(guestBPage.getByTestId("joincall_joincall")).toBeVisible();
|
||||||
|
await guestBPage.getByTestId("joincall_joincall").click();
|
||||||
|
await guestBPage.getByTestId("lobby_joinCall").click();
|
||||||
|
await guestBPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// ACT: add a third user to the call to reproduce the bug
|
||||||
|
// ========
|
||||||
|
const guestC = await browser.newContext({
|
||||||
|
reducedMotion: "reduce",
|
||||||
|
});
|
||||||
|
const guestCPage = await guestC.newPage();
|
||||||
|
let sfuGetCallCount = 0;
|
||||||
|
await guestCPage.route("**/livekit/jwt/sfu/get", async (route) => {
|
||||||
|
sfuGetCallCount++;
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
// Track WebSocket connections
|
||||||
|
let wsConnectionCount = 0;
|
||||||
|
await guestCPage.routeWebSocket("**", (ws) => {
|
||||||
|
// For some reason the interception is not working with the **
|
||||||
|
if (ws.url().includes("livekit/sfu/rtc")) {
|
||||||
|
wsConnectionCount++;
|
||||||
|
}
|
||||||
|
ws.connectToServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
await guestCPage.goto(inviteLink);
|
||||||
|
await guestCPage.getByTestId("joincall_displayName").fill("Invitee");
|
||||||
|
await expect(guestCPage.getByTestId("joincall_joincall")).toBeVisible();
|
||||||
|
await guestCPage.getByTestId("joincall_joincall").click();
|
||||||
|
await guestCPage.getByTestId("lobby_joinCall").click();
|
||||||
|
await guestCPage.getByRole("radio", { name: "Spotlight" }).check();
|
||||||
|
|
||||||
|
await guestCPage.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// ========
|
||||||
|
// the creator leaves the call
|
||||||
|
await creatorPage.getByTestId("incall_leave").click();
|
||||||
|
|
||||||
|
await guestCPage.waitForTimeout(2000);
|
||||||
|
// https://github.com/element-hq/element-call/issues/3344
|
||||||
|
// The app used to request a new jwt token then to reconnect to the SFU
|
||||||
|
expect(wsConnectionCount).toBe(1);
|
||||||
|
expect(sfuGetCallCount).toBe(1);
|
||||||
|
});
|
||||||
@@ -6,12 +6,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import { type Ref, type FC, type HTMLAttributes, type ReactNode } from "react";
|
||||||
type FC,
|
|
||||||
type HTMLAttributes,
|
|
||||||
type ReactNode,
|
|
||||||
forwardRef,
|
|
||||||
} from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Heading, Text } from "@vector-im/compound-web";
|
import { Heading, Text } from "@vector-im/compound-web";
|
||||||
@@ -24,12 +19,17 @@ import { EncryptionLock } from "./room/EncryptionLock";
|
|||||||
import { useMediaQuery } from "./useMediaQuery";
|
import { useMediaQuery } from "./useMediaQuery";
|
||||||
|
|
||||||
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
ref?: Ref<HTMLElement>;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header = forwardRef<HTMLElement, HeaderProps>(
|
export const Header: FC<HeaderProps> = ({
|
||||||
({ children, className, ...rest }, ref) => {
|
ref,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -39,8 +39,7 @@ export const Header = forwardRef<HTMLElement, HeaderProps>(
|
|||||||
{children}
|
{children}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
Header.displayName = "Header";
|
Header.displayName = "Header";
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type ComponentProps, type FC, type MouseEvent } from "react";
|
||||||
type ComponentPropsWithoutRef,
|
|
||||||
forwardRef,
|
|
||||||
type MouseEvent,
|
|
||||||
} from "react";
|
|
||||||
import { Link as CpdLink } from "@vector-im/compound-web";
|
import { Link as CpdLink } from "@vector-im/compound-web";
|
||||||
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
|
import { type LinkProps, useHref, useLinkClickHandler } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@@ -26,31 +22,30 @@ export function useLink(
|
|||||||
return [href, onClick];
|
return [href, onClick];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<ComponentProps<typeof CpdLink>, "href" | "onClick"> & {
|
||||||
ComponentPropsWithoutRef<typeof CpdLink>,
|
to: LinkProps["to"];
|
||||||
"href" | "onClick"
|
state?: unknown;
|
||||||
> & { to: LinkProps["to"]; state?: unknown };
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A version of Compound's link component that integrates with our router setup.
|
* A version of Compound's link component that integrates with our router setup.
|
||||||
* This is only for app-internal links.
|
* This is only for app-internal links.
|
||||||
*/
|
*/
|
||||||
export const Link = forwardRef<HTMLAnchorElement, Props>(function Link(
|
export const Link: FC<Props> = ({ ref, to, state, ...props }) => {
|
||||||
{ to, state, ...props },
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const [path, onClick] = useLink(to, state);
|
const [path, onClick] = useLink(to, state);
|
||||||
return <CpdLink ref={ref} {...props} href={path} onClick={onClick} />;
|
return <CpdLink ref={ref} {...props} href={path} onClick={onClick} />;
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A link to an external web page, made to fit into blocks of text more subtly
|
* A link to an external web page, made to fit into blocks of text more subtly
|
||||||
* than the normal Compound link component.
|
* than the normal Compound link component.
|
||||||
*/
|
*/
|
||||||
export const ExternalLink = forwardRef<
|
export const ExternalLink: FC<ComponentProps<"a">> = ({
|
||||||
HTMLAnchorElement,
|
ref,
|
||||||
ComponentPropsWithoutRef<"a">
|
className,
|
||||||
>(function ExternalLink({ className, children, ...props }, ref) {
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -62,4 +57,4 @@ export const ExternalLink = forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -5,24 +5,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ComponentPropsWithoutRef, forwardRef } from "react";
|
import { type ComponentProps, type FC } from "react";
|
||||||
import { Button } from "@vector-im/compound-web";
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import type { LinkProps } from "react-router-dom";
|
import type { LinkProps } from "react-router-dom";
|
||||||
import { useLink } from "./Link";
|
import { useLink } from "./Link";
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<ComponentProps<typeof Button<"a">>, "as" | "href"> & {
|
||||||
ComponentPropsWithoutRef<typeof Button<"a">>,
|
to: LinkProps["to"];
|
||||||
"as" | "href"
|
state?: unknown;
|
||||||
> & { to: LinkProps["to"]; state?: unknown };
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A version of Compound's button component that acts as a link and integrates
|
* A version of Compound's button component that acts as a link and integrates
|
||||||
* with our router setup.
|
* with our router setup.
|
||||||
*/
|
*/
|
||||||
export const LinkButton = forwardRef<HTMLAnchorElement, Props>(
|
export const LinkButton: FC<Props> = ({ ref, to, state, ...props }) => {
|
||||||
function LinkButton({ to, state, ...props }, ref) {
|
|
||||||
const [path, onClick] = useLink(to, state);
|
const [path, onClick] = useLink(to, state);
|
||||||
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
return <Button as="a" ref={ref} {...props} href={path} onClick={onClick} />;
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|||||||
@@ -6,18 +6,23 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { type FormEventHandler, forwardRef, type ReactNode } from "react";
|
import {
|
||||||
|
type FC,
|
||||||
|
type Ref,
|
||||||
|
type FormEventHandler,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import styles from "./Form.module.css";
|
import styles from "./Form.module.css";
|
||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
|
ref?: Ref<HTMLFormElement>;
|
||||||
className: string;
|
className: string;
|
||||||
onSubmit: FormEventHandler<HTMLFormElement>;
|
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||||
children: ReactNode[];
|
children: ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Form = forwardRef<HTMLFormElement, FormProps>(
|
export const Form: FC<FormProps> = ({ ref, children, className, onSubmit }) => {
|
||||||
({ children, className, onSubmit }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
@@ -27,7 +32,6 @@ export const Form = forwardRef<HTMLFormElement, FormProps>(
|
|||||||
{children}
|
{children}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
Form.displayName = "Form";
|
Form.displayName = "Form";
|
||||||
|
|||||||
@@ -18,11 +18,10 @@ import {
|
|||||||
type ComponentType,
|
type ComponentType,
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
type FC,
|
type FC,
|
||||||
type LegacyRef,
|
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
|
type Ref,
|
||||||
type SetStateAction,
|
type SetStateAction,
|
||||||
createContext,
|
createContext,
|
||||||
forwardRef,
|
|
||||||
memo,
|
memo,
|
||||||
use,
|
use,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -162,7 +161,7 @@ const windowHeightObservable$ = fromEvent(window, "resize").pipe(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
||||||
ref: LegacyRef<R>;
|
ref?: Ref<R>;
|
||||||
model: LayoutModel;
|
model: LayoutModel;
|
||||||
/**
|
/**
|
||||||
* Component creating an invisible "slot" for a tile to go in.
|
* Component creating an invisible "slot" for a tile to go in.
|
||||||
@@ -171,7 +170,7 @@ export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TileProps<Model, R extends HTMLElement> {
|
export interface TileProps<Model, R extends HTMLElement> {
|
||||||
ref: LegacyRef<R>;
|
ref?: Ref<R>;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: ComponentProps<typeof animated.div>["style"];
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
/**
|
/**
|
||||||
@@ -297,14 +296,13 @@ export function Grid<
|
|||||||
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
|
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
|
||||||
const LayoutMemo = useMemo(
|
const LayoutMemo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
memo(
|
memo(function LayoutMemo({
|
||||||
forwardRef<
|
ref,
|
||||||
LayoutRef,
|
Layout,
|
||||||
LayoutMemoProps<LayoutModel, TileModel, LayoutRef>
|
...props
|
||||||
>(function LayoutMemo({ Layout, ...props }, ref): ReactNode {
|
}: LayoutMemoProps<LayoutModel, TileModel, LayoutRef>): ReactNode {
|
||||||
return <Layout {...props} ref={ref} />;
|
return <Layout {...props} ref={ref} />;
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type CSSProperties, forwardRef, useCallback, useMemo } from "react";
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { distinctUntilChanged } from "rxjs";
|
import { distinctUntilChanged } from "rxjs";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
|
||||||
@@ -33,7 +38,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
|
|
||||||
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
||||||
// lives
|
// lives
|
||||||
fixed: forwardRef(function GridLayoutFixed({ model, Slot }, ref) {
|
fixed: function GridLayoutFixed({ ref, model, Slot }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const alignment = useObservableEagerState(
|
const alignment = useObservableEagerState(
|
||||||
useInitial(() =>
|
useInitial(() =>
|
||||||
@@ -68,10 +73,10 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
// The scrolling part of the layout is where all the grid tiles live
|
// The scrolling part of the layout is where all the grid tiles live
|
||||||
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
|
scrolling: function GridLayout({ ref, model, Slot }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useVisibleTiles(model.setVisibleTiles);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
const { width, height: minHeight } = useObservableEagerState(minBounds$);
|
const { width, height: minHeight } = useObservableEagerState(minBounds$);
|
||||||
@@ -98,5 +103,5 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef, useCallback, useMemo } from "react";
|
import { type ReactNode, useCallback, useMemo } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
}) => ({
|
}) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function OneOnOneLayoutFixed(_props, ref) {
|
fixed: function OneOnOneLayoutFixed({ ref }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
return <div ref={ref} />;
|
return <div ref={ref} />;
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
|
scrolling: function OneOnOneLayoutScrolling({ ref, model, Slot }): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const { width, height } = useObservableEagerState(minBounds$);
|
const { width, height } = useObservableEagerState(minBounds$);
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
||||||
@@ -66,5 +66,5 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
</Slot>
|
</Slot>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef, useCallback } from "react";
|
import { type ReactNode, useCallback } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
|
||||||
import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
|
import { type SpotlightExpandedLayout as SpotlightExpandedLayoutModel } from "../state/CallViewModel";
|
||||||
@@ -22,10 +22,11 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
> = ({ pipAlignment$ }) => ({
|
> = ({ pipAlignment$ }) => ({
|
||||||
scrollingOnTop: true,
|
scrollingOnTop: true,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
|
fixed: function SpotlightExpandedLayoutFixed({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,12 +38,13 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function SpotlightExpandedLayoutScrolling(
|
scrolling: function SpotlightExpandedLayoutScrolling({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
||||||
|
|
||||||
@@ -69,5 +71,5 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
@@ -24,10 +24,11 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
> = ({ minBounds$ }) => ({
|
> = ({ minBounds$ }) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightLandscapeLayoutFixed(
|
fixed: function SpotlightLandscapeLayoutFixed({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useObservableEagerState(minBounds$);
|
useObservableEagerState(minBounds$);
|
||||||
|
|
||||||
@@ -43,12 +44,13 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
<div className={styles.grid} />
|
<div className={styles.grid} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function SpotlightLandscapeLayoutScrolling(
|
scrolling: function SpotlightLandscapeLayoutScrolling({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useVisibleTiles(model.setVisibleTiles);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
useObservableEagerState(minBounds$);
|
useObservableEagerState(minBounds$);
|
||||||
@@ -69,5 +71,5 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type CSSProperties, forwardRef } from "react";
|
import { type ReactNode, type CSSProperties } from "react";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
@@ -30,10 +30,11 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
> = ({ minBounds$ }) => ({
|
> = ({ minBounds$ }) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightPortraitLayoutFixed(
|
fixed: function SpotlightPortraitLayoutFixed({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,12 +48,13 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
|
||||||
scrolling: forwardRef(function SpotlightPortraitLayoutScrolling(
|
scrolling: function SpotlightPortraitLayoutScrolling({
|
||||||
{ model, Slot },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
model,
|
||||||
|
Slot,
|
||||||
|
}): ReactNode {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useVisibleTiles(model.setVisibleTiles);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
const { width } = useObservableEagerState(minBounds$);
|
const { width } = useObservableEagerState(minBounds$);
|
||||||
@@ -90,5 +92,5 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ body {
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* This prohibits the view to scroll for pages smaller than 122px in width
|
||||||
|
we use this for mobile pip webviews */
|
||||||
|
.no-scroll-body {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* We use this to not render the page at all until we know the theme.*/
|
/* We use this to not render the page at all until we know the theme.*/
|
||||||
.no-theme {
|
.no-theme {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type FC,
|
type FC,
|
||||||
type ForwardedRef,
|
type ForwardedRef,
|
||||||
forwardRef,
|
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useId,
|
useId,
|
||||||
type JSX,
|
type JSX,
|
||||||
|
type Ref,
|
||||||
} from "react";
|
} from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ function Field({ children, className }: FieldProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface InputFieldProps {
|
interface InputFieldProps {
|
||||||
|
ref?: Ref<HTMLInputElement | HTMLTextAreaElement>;
|
||||||
label?: string;
|
label?: string;
|
||||||
type: string;
|
type: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
@@ -78,12 +79,8 @@ interface InputFieldProps {
|
|||||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InputField = forwardRef<
|
export const InputField: FC<InputFieldProps> = ({
|
||||||
HTMLInputElement | HTMLTextAreaElement,
|
ref,
|
||||||
InputFieldProps
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
@@ -95,9 +92,7 @@ export const InputField = forwardRef<
|
|||||||
disabled,
|
disabled,
|
||||||
min,
|
min,
|
||||||
...rest
|
...rest
|
||||||
},
|
}) => {
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const descriptionId = useId();
|
const descriptionId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,8 +153,7 @@ export const InputField = forwardRef<
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
InputField.displayName = "InputField";
|
InputField.displayName = "InputField";
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,16 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// This CSS is the only way we could find to not make element call scroll for
|
||||||
|
// viewport sizes smaller than 122px width. (It is actually this exact number: 122px
|
||||||
|
// tested on different devices...)
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.classList.add("no-scroll-body");
|
||||||
|
return (): void => {
|
||||||
|
document.body.classList.remove("no-scroll-body");
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.rtcSession = rtcSession;
|
window.rtcSession = rtcSession;
|
||||||
return (): void => {
|
return (): void => {
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import { type MatrixClient } from "matrix-js-sdk";
|
|||||||
import {
|
import {
|
||||||
type FC,
|
type FC,
|
||||||
type PointerEvent,
|
type PointerEvent,
|
||||||
type PropsWithoutRef,
|
|
||||||
type TouchEvent,
|
type TouchEvent,
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -521,13 +519,14 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
|
|
||||||
const Tile = useMemo(
|
const Tile = useMemo(
|
||||||
() =>
|
() =>
|
||||||
forwardRef<
|
function Tile({
|
||||||
HTMLDivElement,
|
|
||||||
PropsWithoutRef<TileProps<TileViewModel, HTMLDivElement>>
|
|
||||||
>(function Tile(
|
|
||||||
{ className, style, targetWidth, targetHeight, model },
|
|
||||||
ref,
|
ref,
|
||||||
) {
|
className,
|
||||||
|
style,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight,
|
||||||
|
model,
|
||||||
|
}: TileProps<TileViewModel, HTMLDivElement>): ReactNode {
|
||||||
const spotlightExpanded = useObservableEagerState(
|
const spotlightExpanded = useObservableEagerState(
|
||||||
vm.spotlightExpanded$,
|
vm.spotlightExpanded$,
|
||||||
);
|
);
|
||||||
@@ -565,7 +564,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
[vm, openProfile],
|
[vm, openProfile],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
|
import { type LivekitFocus, isLivekitFocus } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
@@ -24,27 +24,22 @@ import { useTypedEventEmitterState } from "../useEvents";
|
|||||||
export function useActiveLivekitFocus(
|
export function useActiveLivekitFocus(
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
): LivekitFocus | undefined {
|
): LivekitFocus | undefined {
|
||||||
const activeFocus = useTypedEventEmitterState(
|
const prevActiveFocus = useRef<LivekitFocus | undefined>(undefined);
|
||||||
|
return useTypedEventEmitterState(
|
||||||
rtcSession,
|
rtcSession,
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const f = rtcSession.getActiveFocus();
|
const f = rtcSession.getActiveFocus();
|
||||||
// Only handle foci with type="livekit" for now.
|
// Only handle foci with type="livekit" for now.
|
||||||
return !!f && isLivekitFocus(f) ? f : undefined;
|
if (f && isLivekitFocus(f) && !deepCompare(f, prevActiveFocus.current)) {
|
||||||
|
const oldestMembership = rtcSession.getOldestMembership();
|
||||||
|
logger.info(
|
||||||
|
`Got new active focus from membership: ${oldestMembership?.sender}/${oldestMembership?.deviceId}.
|
||||||
|
Updated focus (focus switch) from ${JSON.stringify(prevActiveFocus.current)} to ${JSON.stringify(f)}`,
|
||||||
|
);
|
||||||
|
prevActiveFocus.current = f;
|
||||||
|
}
|
||||||
|
return prevActiveFocus.current;
|
||||||
}, [rtcSession]),
|
}, [rtcSession]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevActiveFocus = useRef(activeFocus);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!deepCompare(activeFocus, prevActiveFocus.current)) {
|
|
||||||
const oldestMembership = rtcSession.getOldestMembership();
|
|
||||||
logger.warn(
|
|
||||||
`Got new active focus from membership: ${oldestMembership?.sender}/${oldestMembership?.deviceId}.
|
|
||||||
Updated focus (focus switch) from ${JSON.stringify(prevActiveFocus.current)} to ${JSON.stringify(activeFocus)}`,
|
|
||||||
);
|
|
||||||
prevActiveFocus.current = activeFocus;
|
|
||||||
}
|
|
||||||
}, [activeFocus, rtcSession]);
|
|
||||||
|
|
||||||
return activeFocus;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
||||||
} from "../controls";
|
} from "../controls";
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
|
import { platform } from "../Platform";
|
||||||
|
|
||||||
// This hardcoded id is used in EX ios! It can only be changed in coordination with
|
// This hardcoded id is used in EX ios! It can only be changed in coordination with
|
||||||
// the ios swift team.
|
// the ios swift team.
|
||||||
@@ -90,9 +91,8 @@ export interface MediaDevice<Label, Selected> {
|
|||||||
* - Only show the earpiece toggle option if the earpiece is available:
|
* - Only show the earpiece toggle option if the earpiece is available:
|
||||||
* `availableOutputDevices$.includes((d)=>d.forEarpiece)`
|
* `availableOutputDevices$.includes((d)=>d.forEarpiece)`
|
||||||
*/
|
*/
|
||||||
export const iosDeviceMenu$ = navigator.userAgent.includes("iPhone")
|
export const iosDeviceMenu$ =
|
||||||
? of(true)
|
platform === "ios" ? of(true) : alwaysShowIphoneEarpieceSetting.value$;
|
||||||
: alwaysShowIphoneEarpieceSetting.value$;
|
|
||||||
|
|
||||||
function availableRawDevices$(
|
function availableRawDevices$(
|
||||||
kind: MediaDeviceKind,
|
kind: MediaDeviceKind,
|
||||||
@@ -273,21 +273,21 @@ class ControlledAudioOutput
|
|||||||
this.deviceSelection$.next(id);
|
this.deviceSelection$.next(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly selected$ = combineLatest([
|
public readonly selected$ = combineLatest(
|
||||||
|
[
|
||||||
this.available$,
|
this.available$,
|
||||||
merge(
|
merge(
|
||||||
controlledOutputSelection$.pipe(startWith(undefined)),
|
controlledOutputSelection$.pipe(startWith(undefined)),
|
||||||
this.deviceSelection$,
|
this.deviceSelection$,
|
||||||
),
|
),
|
||||||
]).pipe(
|
],
|
||||||
map(([available, selectId]) => {
|
(available, preferredId) => {
|
||||||
const id = selectId ?? available.keys().next().value;
|
const id = preferredId ?? available.keys().next().value;
|
||||||
return id
|
return id === undefined
|
||||||
? { id, virtualEarpiece: id === EARPIECE_CONFIG_ID }
|
? undefined
|
||||||
: undefined;
|
: { id, virtualEarpiece: id === EARPIECE_CONFIG_ID };
|
||||||
}),
|
},
|
||||||
this.scope.state(),
|
).pipe(this.scope.state());
|
||||||
);
|
|
||||||
|
|
||||||
public constructor(private readonly scope: ObservableScope) {
|
public constructor(private readonly scope: ObservableScope) {
|
||||||
this.selected$.subscribe((device) => {
|
this.selected$.subscribe((device) => {
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
|
type FC,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
forwardRef,
|
type Ref,
|
||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -50,6 +51,7 @@ import { useMergedRefs } from "../useMergedRefs";
|
|||||||
import { useReactionsSender } from "../reactions/useReactionsSender";
|
import { useReactionsSender } from "../reactions/useReactionsSender";
|
||||||
|
|
||||||
interface TileProps {
|
interface TileProps {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: ComponentProps<typeof animated.div>["style"];
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
targetWidth: number;
|
targetWidth: number;
|
||||||
@@ -66,9 +68,8 @@ interface UserMediaTileProps extends TileProps {
|
|||||||
menuEnd?: ReactNode;
|
menuEnd?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||||
(
|
ref,
|
||||||
{
|
|
||||||
vm,
|
vm,
|
||||||
showSpeakingIndicators,
|
showSpeakingIndicators,
|
||||||
locallyMuted,
|
locallyMuted,
|
||||||
@@ -77,9 +78,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
className,
|
className,
|
||||||
displayName,
|
displayName,
|
||||||
...props
|
...props
|
||||||
},
|
}) => {
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const { toggleRaisedHand } = useReactionsSender();
|
const { toggleRaisedHand } = useReactionsSender();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const video = useObservableEagerState(vm.video$);
|
const video = useObservableEagerState(vm.video$);
|
||||||
@@ -190,8 +189,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
{menu}
|
{menu}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
UserMediaTile.displayName = "UserMediaTile";
|
UserMediaTile.displayName = "UserMediaTile";
|
||||||
|
|
||||||
@@ -200,8 +198,12 @@ interface LocalUserMediaTileProps extends TileProps {
|
|||||||
onOpenProfile: (() => void) | null;
|
onOpenProfile: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
|
const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||||
({ vm, onOpenProfile, ...props }, ref) => {
|
ref,
|
||||||
|
vm,
|
||||||
|
onOpenProfile,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mirror = useObservableEagerState(vm.mirror$);
|
const mirror = useObservableEagerState(vm.mirror$);
|
||||||
const alwaysShow = useObservableEagerState(vm.alwaysShow$);
|
const alwaysShow = useObservableEagerState(vm.alwaysShow$);
|
||||||
@@ -240,8 +242,7 @@ const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
LocalUserMediaTile.displayName = "LocalUserMediaTile";
|
LocalUserMediaTile.displayName = "LocalUserMediaTile";
|
||||||
|
|
||||||
@@ -249,10 +250,11 @@ interface RemoteUserMediaTileProps extends TileProps {
|
|||||||
vm: RemoteUserMediaViewModel;
|
vm: RemoteUserMediaViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RemoteUserMediaTile = forwardRef<
|
const RemoteUserMediaTile: FC<RemoteUserMediaTileProps> = ({
|
||||||
HTMLDivElement,
|
ref,
|
||||||
RemoteUserMediaTileProps
|
vm,
|
||||||
>(({ vm, ...props }, ref) => {
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const locallyMuted = useObservableEagerState(vm.locallyMuted$);
|
const locallyMuted = useObservableEagerState(vm.locallyMuted$);
|
||||||
const localVolume = useObservableEagerState(vm.localVolume$);
|
const localVolume = useObservableEagerState(vm.localVolume$);
|
||||||
@@ -303,11 +305,12 @@ const RemoteUserMediaTile = forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
RemoteUserMediaTile.displayName = "RemoteUserMediaTile";
|
RemoteUserMediaTile.displayName = "RemoteUserMediaTile";
|
||||||
|
|
||||||
interface GridTileProps {
|
interface GridTileProps {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
vm: GridTileViewModel;
|
vm: GridTileViewModel;
|
||||||
onOpenProfile: (() => void) | null;
|
onOpenProfile: (() => void) | null;
|
||||||
targetWidth: number;
|
targetWidth: number;
|
||||||
@@ -317,8 +320,12 @@ interface GridTileProps {
|
|||||||
showSpeakingIndicators: boolean;
|
showSpeakingIndicators: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
|
export const GridTile: FC<GridTileProps> = ({
|
||||||
({ vm, onOpenProfile, ...props }, theirRef) => {
|
ref: theirRef,
|
||||||
|
vm,
|
||||||
|
onOpenProfile,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const media = useObservableEagerState(vm.media$);
|
const media = useObservableEagerState(vm.media$);
|
||||||
@@ -344,7 +351,6 @@ export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
GridTile.displayName = "GridTile";
|
GridTile.displayName = "GridTile";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
import { type TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||||
import { animated } from "@react-spring/web";
|
import { animated } from "@react-spring/web";
|
||||||
import { type RoomMember } from "matrix-js-sdk";
|
import { type RoomMember } from "matrix-js-sdk";
|
||||||
import { type ComponentProps, type ReactNode, forwardRef } from "react";
|
import { type FC, type ComponentProps, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { VideoTrack } from "@livekit/components-react";
|
import { VideoTrack } from "@livekit/components-react";
|
||||||
@@ -47,9 +47,8 @@ interface Props extends ComponentProps<typeof animated.div> {
|
|||||||
videoStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
videoStreamStats?: RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaView = forwardRef<HTMLDivElement, Props>(
|
export const MediaView: FC<Props> = ({
|
||||||
(
|
ref,
|
||||||
{
|
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
targetWidth,
|
targetWidth,
|
||||||
@@ -71,9 +70,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
audioStreamStats,
|
audioStreamStats,
|
||||||
videoStreamStats,
|
videoStreamStats,
|
||||||
...props
|
...props
|
||||||
},
|
}) => {
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
|
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
|
||||||
|
|
||||||
@@ -182,7 +179,6 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
</div>
|
</div>
|
||||||
</animated.div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
MediaView.displayName = "MediaView";
|
MediaView.displayName = "MediaView";
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
|
type FC,
|
||||||
|
type Ref,
|
||||||
type RefAttributes,
|
type RefAttributes,
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -44,6 +45,7 @@ import { useLatest } from "../useLatest";
|
|||||||
import { type SpotlightTileViewModel } from "../state/TileViewModel";
|
import { type SpotlightTileViewModel } from "../state/TileViewModel";
|
||||||
|
|
||||||
interface SpotlightItemBaseProps {
|
interface SpotlightItemBaseProps {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
className?: string;
|
className?: string;
|
||||||
"data-id": string;
|
"data-id": string;
|
||||||
targetWidth: number;
|
targetWidth: number;
|
||||||
@@ -67,13 +69,13 @@ interface SpotlightLocalUserMediaItemProps
|
|||||||
vm: LocalUserMediaViewModel;
|
vm: LocalUserMediaViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightLocalUserMediaItem = forwardRef<
|
const SpotlightLocalUserMediaItem: FC<SpotlightLocalUserMediaItemProps> = ({
|
||||||
HTMLDivElement,
|
vm,
|
||||||
SpotlightLocalUserMediaItemProps
|
...props
|
||||||
>(({ vm, ...props }, ref) => {
|
}) => {
|
||||||
const mirror = useObservableEagerState(vm.mirror$);
|
const mirror = useObservableEagerState(vm.mirror$);
|
||||||
return <MediaView ref={ref} mirror={mirror} {...props} />;
|
return <MediaView mirror={mirror} {...props} />;
|
||||||
});
|
};
|
||||||
|
|
||||||
SpotlightLocalUserMediaItem.displayName = "SpotlightLocalUserMediaItem";
|
SpotlightLocalUserMediaItem.displayName = "SpotlightLocalUserMediaItem";
|
||||||
|
|
||||||
@@ -81,16 +83,15 @@ interface SpotlightUserMediaItemProps extends SpotlightItemBaseProps {
|
|||||||
vm: UserMediaViewModel;
|
vm: UserMediaViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightUserMediaItem = forwardRef<
|
const SpotlightUserMediaItem: FC<SpotlightUserMediaItemProps> = ({
|
||||||
HTMLDivElement,
|
vm,
|
||||||
SpotlightUserMediaItemProps
|
...props
|
||||||
>(({ vm, ...props }, ref) => {
|
}) => {
|
||||||
const videoEnabled = useObservableEagerState(vm.videoEnabled$);
|
const videoEnabled = useObservableEagerState(vm.videoEnabled$);
|
||||||
const cropVideo = useObservableEagerState(vm.cropVideo$);
|
const cropVideo = useObservableEagerState(vm.cropVideo$);
|
||||||
|
|
||||||
const baseProps: SpotlightUserMediaItemBaseProps &
|
const baseProps: SpotlightUserMediaItemBaseProps &
|
||||||
RefAttributes<HTMLDivElement> = {
|
RefAttributes<HTMLDivElement> = {
|
||||||
ref,
|
|
||||||
videoEnabled,
|
videoEnabled,
|
||||||
videoFit: cropVideo ? "cover" : "contain",
|
videoFit: cropVideo ? "cover" : "contain",
|
||||||
...props,
|
...props,
|
||||||
@@ -101,11 +102,12 @@ const SpotlightUserMediaItem = forwardRef<
|
|||||||
) : (
|
) : (
|
||||||
<MediaView mirror={false} {...baseProps} />
|
<MediaView mirror={false} {...baseProps} />
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
SpotlightUserMediaItem.displayName = "SpotlightUserMediaItem";
|
SpotlightUserMediaItem.displayName = "SpotlightUserMediaItem";
|
||||||
|
|
||||||
interface SpotlightItemProps {
|
interface SpotlightItemProps {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
vm: MediaViewModel;
|
vm: MediaViewModel;
|
||||||
targetWidth: number;
|
targetWidth: number;
|
||||||
targetHeight: number;
|
targetHeight: number;
|
||||||
@@ -117,18 +119,15 @@ interface SpotlightItemProps {
|
|||||||
"aria-hidden"?: boolean;
|
"aria-hidden"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
const SpotlightItem: FC<SpotlightItemProps> = ({
|
||||||
(
|
ref: theirRef,
|
||||||
{
|
|
||||||
vm,
|
vm,
|
||||||
targetWidth,
|
targetWidth,
|
||||||
targetHeight,
|
targetHeight,
|
||||||
intersectionObserver$,
|
intersectionObserver$,
|
||||||
snap,
|
snap,
|
||||||
"aria-hidden": ariaHidden,
|
"aria-hidden": ariaHidden,
|
||||||
},
|
}) => {
|
||||||
theirRef,
|
|
||||||
) => {
|
|
||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const displayName = useObservableEagerState(vm.displayname$);
|
const displayName = useObservableEagerState(vm.displayname$);
|
||||||
@@ -167,21 +166,16 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return vm instanceof ScreenShareViewModel ? (
|
return vm instanceof ScreenShareViewModel ? (
|
||||||
<MediaView
|
<MediaView videoEnabled videoFit="contain" mirror={false} {...baseProps} />
|
||||||
videoEnabled
|
|
||||||
videoFit="contain"
|
|
||||||
mirror={false}
|
|
||||||
{...baseProps}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<SpotlightUserMediaItem vm={vm} {...baseProps} />
|
<SpotlightUserMediaItem vm={vm} {...baseProps} />
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
SpotlightItem.displayName = "SpotlightItem";
|
SpotlightItem.displayName = "SpotlightItem";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
vm: SpotlightTileViewModel;
|
vm: SpotlightTileViewModel;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
onToggleExpanded: (() => void) | null;
|
onToggleExpanded: (() => void) | null;
|
||||||
@@ -192,9 +186,8 @@ interface Props {
|
|||||||
style?: ComponentProps<typeof animated.div>["style"];
|
style?: ComponentProps<typeof animated.div>["style"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
export const SpotlightTile: FC<Props> = ({
|
||||||
(
|
ref: theirRef,
|
||||||
{
|
|
||||||
vm,
|
vm,
|
||||||
expanded,
|
expanded,
|
||||||
onToggleExpanded,
|
onToggleExpanded,
|
||||||
@@ -203,17 +196,13 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
showIndicators,
|
showIndicators,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
},
|
}) => {
|
||||||
theirRef,
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
|
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const maximised = useObservableEagerState(vm.maximised$);
|
const maximised = useObservableEagerState(vm.maximised$);
|
||||||
const media = useObservableEagerState(vm.media$);
|
const media = useObservableEagerState(vm.media$);
|
||||||
const [visibleId, setVisibleId] = useState<string | undefined>(
|
const [visibleId, setVisibleId] = useState<string | undefined>(media[0]?.id);
|
||||||
media[0]?.id,
|
|
||||||
);
|
|
||||||
const latestMedia = useLatest(media);
|
const latestMedia = useLatest(media);
|
||||||
const latestVisibleId = useLatest(visibleId);
|
const latestVisibleId = useLatest(visibleId);
|
||||||
const visibleIndex = media.findIndex((vm) => vm.id === visibleId);
|
const visibleIndex = media.findIndex((vm) => vm.id === visibleId);
|
||||||
@@ -243,9 +232,7 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
|
|
||||||
const [scrollToId, setScrollToId] = useReactiveState<string | null>(
|
const [scrollToId, setScrollToId] = useReactiveState<string | null>(
|
||||||
(prev) =>
|
(prev) =>
|
||||||
prev == null ||
|
prev == null || prev === visibleId || media.every((vm) => vm.id !== prev)
|
||||||
prev === visibleId ||
|
|
||||||
media.every((vm) => vm.id !== prev)
|
|
||||||
? null
|
? null
|
||||||
: prev,
|
: prev,
|
||||||
[visibleId],
|
[visibleId],
|
||||||
@@ -341,7 +328,6 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
)}
|
)}
|
||||||
</animated.div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
|
||||||
SpotlightTile.displayName = "SpotlightTile";
|
SpotlightTile.displayName = "SpotlightTile";
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MutableRefObject, type RefCallback, useCallback } from "react";
|
import { type RefCallback, type RefObject, useCallback } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines multiple refs into one, useful for attaching multiple refs to the
|
* Combines multiple refs into one, useful for attaching multiple refs to the
|
||||||
* same DOM node.
|
* same DOM node.
|
||||||
*/
|
*/
|
||||||
export const useMergedRefs = <T>(
|
export const useMergedRefs = <T>(
|
||||||
...refs: (MutableRefObject<T | null> | RefCallback<T | null> | null)[]
|
...refs: (RefObject<T | null> | RefCallback<T | null> | null | undefined)[]
|
||||||
): RefCallback<T | null> =>
|
): RefCallback<T | null> =>
|
||||||
useCallback(
|
useCallback(
|
||||||
(value) =>
|
(value) =>
|
||||||
refs.forEach((ref) => {
|
refs.forEach((ref) => {
|
||||||
if (typeof ref === "function") {
|
if (typeof ref === "function") {
|
||||||
ref(value);
|
ref(value);
|
||||||
} else if (ref !== null) {
|
} else if (ref) {
|
||||||
ref.current = value;
|
ref.current = value;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -2702,37 +2702,37 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@livekit/components-core@npm:0.12.7, @livekit/components-core@npm:^0.12.0":
|
"@livekit/components-core@npm:0.12.8, @livekit/components-core@npm:^0.12.0":
|
||||||
version: 0.12.7
|
version: 0.12.8
|
||||||
resolution: "@livekit/components-core@npm:0.12.7"
|
resolution: "@livekit/components-core@npm:0.12.8"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/dom": "npm:1.6.13"
|
"@floating-ui/dom": "npm:1.6.13"
|
||||||
loglevel: "npm:1.9.1"
|
loglevel: "npm:1.9.1"
|
||||||
rxjs: "npm:7.8.2"
|
rxjs: "npm:7.8.2"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
livekit-client: ^2.13.1
|
livekit-client: ^2.13.3
|
||||||
tslib: ^2.6.2
|
tslib: ^2.6.2
|
||||||
checksum: 10c0/11024e120f2b1b81688d507dc8441f663730956d4c79f66014a9f19a3723fcdee9517a09239f21d501067cc299bf97659655404048c415314c39bf6af5d8ab9d
|
checksum: 10c0/68c471b2f1319fbbfff05138b55442cc2ba575bc8506b3e71bb222fdf7026041f88c38e832227bbcc37edd4d3936f997c1b54ce7b27d1971f960794f049c9684
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@livekit/components-react@npm:^2.0.0":
|
"@livekit/components-react@npm:^2.0.0":
|
||||||
version: 2.9.10
|
version: 2.9.12
|
||||||
resolution: "@livekit/components-react@npm:2.9.10"
|
resolution: "@livekit/components-react@npm:2.9.12"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@livekit/components-core": "npm:0.12.7"
|
"@livekit/components-core": "npm:0.12.8"
|
||||||
clsx: "npm:2.1.1"
|
clsx: "npm:2.1.1"
|
||||||
usehooks-ts: "npm:3.1.1"
|
usehooks-ts: "npm:3.1.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@livekit/krisp-noise-filter": ^0.2.12
|
"@livekit/krisp-noise-filter": ^0.2.12
|
||||||
livekit-client: ^2.13.1
|
livekit-client: ^2.13.3
|
||||||
react: ">=18"
|
react: ">=18"
|
||||||
react-dom: ">=18"
|
react-dom: ">=18"
|
||||||
tslib: ^2.6.2
|
tslib: ^2.6.2
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
"@livekit/krisp-noise-filter":
|
"@livekit/krisp-noise-filter":
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10c0/86fc78d7b2d97540c43592435a65ac38b43fca9923af000aa90d884bd35794e18d202b7d915f44b63087abea5c94c37d6980772ed722226904262b2cddbec107
|
checksum: 10c0/445099ff7d335f8b9908e814e327d43477ea19fb93c428fadb0bf2aa56e323171af10fcc9493f686ad5060fb75e0d5c66632e55cd648a76fafe146827c53bd87
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user