mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-17 20:39:19 +00:00
Performance: Avoid re-rendering context menus so often
A change in a tile's speaking indicator could cause its entire tree of context menu components to re-render, which is expensive. Isolating the behavior subscriptions in their own component avoids this.
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { type animated } from "@react-spring/web";
|
||||
import classNames from "classnames";
|
||||
@@ -105,20 +106,22 @@ interface UserMediaTileProps extends TileProps {
|
||||
playbackMuted: boolean;
|
||||
waitingForMedia?: boolean;
|
||||
primaryButton?: ReactNode;
|
||||
menuStart?: ReactNode;
|
||||
menuEnd?: ReactNode;
|
||||
focusUrl: string | undefined;
|
||||
}
|
||||
|
||||
const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
/**
|
||||
* A user media tile without a context menu.
|
||||
*/
|
||||
// The context menu is kept separate from this component for performance
|
||||
// reasons (c.f. UserMediaTile)
|
||||
const UserMediaTileInner: FC<UserMediaTileProps & { menu: ReactNode }> = ({
|
||||
ref,
|
||||
vm,
|
||||
showSpeakingIndicators,
|
||||
playbackMuted,
|
||||
waitingForMedia,
|
||||
primaryButton,
|
||||
menuStart,
|
||||
menuEnd,
|
||||
menu,
|
||||
className,
|
||||
focusUrl,
|
||||
displayName,
|
||||
@@ -166,24 +169,26 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
: t("microphone_off");
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menu = (
|
||||
<>
|
||||
{menuStart}
|
||||
{/*
|
||||
No additional menu item (used to be the manual fit to frame.
|
||||
Placeholder for future menu items that should be placed here.
|
||||
*/}
|
||||
{menuEnd}
|
||||
</>
|
||||
const menuTrigger = useMemo(
|
||||
() => (
|
||||
<button
|
||||
aria-label={t("common.options")}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<OverflowHorizontalIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
),
|
||||
[t, focusable],
|
||||
);
|
||||
|
||||
const raisedHandOnClick = vm.local
|
||||
? (): void => void toggleRaisedHand()
|
||||
: undefined;
|
||||
const raisedHandOnClick = useMemo(
|
||||
() => (vm.local ? (): void => void toggleRaisedHand() : undefined),
|
||||
[vm.local, toggleRaisedHand],
|
||||
);
|
||||
|
||||
const showSpeaking = showSpeakingIndicators && speaking;
|
||||
|
||||
const tile = (
|
||||
return (
|
||||
<MediaView
|
||||
ref={ref}
|
||||
video={video}
|
||||
@@ -213,14 +218,7 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
title={displayName}
|
||||
trigger={
|
||||
<button
|
||||
aria-label={t("common.options")}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<OverflowHorizontalIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
}
|
||||
trigger={menuTrigger}
|
||||
side="left"
|
||||
align="start"
|
||||
>
|
||||
@@ -241,9 +239,37 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A user media tile enhanced with a context menu.
|
||||
*/
|
||||
const UserMediaTile: FC<
|
||||
UserMediaTileProps & { menuStart?: ReactNode; menuEnd?: ReactNode }
|
||||
> = ({ menuStart, menuEnd, ...props }) => {
|
||||
const menu = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{menuStart}
|
||||
{/*
|
||||
No additional menu item (used to be the manual fit to frame.
|
||||
Placeholder for future menu items that should be placed here.
|
||||
*/}
|
||||
{menuEnd}
|
||||
</>
|
||||
),
|
||||
[menuStart, menuEnd],
|
||||
);
|
||||
|
||||
// ContextMenu is expensive to render, so we avoid subscribing to any
|
||||
// frequently-changing behaviors here and instead keep them isolated in the
|
||||
// UserMediaTileInner component
|
||||
return (
|
||||
<ContextMenu title={displayName} trigger={tile} hasAccessibleAlternative>
|
||||
<ContextMenu
|
||||
title={props.displayName}
|
||||
trigger={<UserMediaTileInner {...props} menu={menu} />}
|
||||
hasAccessibleAlternative
|
||||
>
|
||||
{menu}
|
||||
</ContextMenu>
|
||||
);
|
||||
@@ -279,6 +305,29 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
[vm, latestAlwaysShow],
|
||||
);
|
||||
|
||||
const menuStart = useMemo(
|
||||
() => (
|
||||
<ToggleMenuItem
|
||||
Icon={VisibilityOnIcon}
|
||||
label={t("video_tile.always_show")}
|
||||
checked={alwaysShow}
|
||||
onSelect={onSelectAlwaysShow}
|
||||
/>
|
||||
),
|
||||
[t, alwaysShow, onSelectAlwaysShow],
|
||||
);
|
||||
const menuEnd = useMemo(
|
||||
() =>
|
||||
onOpenProfile && (
|
||||
<MenuItem
|
||||
Icon={UserProfileIcon}
|
||||
label={t("common.profile")}
|
||||
onSelect={onOpenProfile}
|
||||
/>
|
||||
),
|
||||
[t, onOpenProfile],
|
||||
);
|
||||
|
||||
return (
|
||||
<UserMediaTile
|
||||
ref={ref}
|
||||
@@ -297,23 +346,8 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
menuStart={
|
||||
<ToggleMenuItem
|
||||
Icon={VisibilityOnIcon}
|
||||
label={t("video_tile.always_show")}
|
||||
checked={alwaysShow}
|
||||
onSelect={onSelectAlwaysShow}
|
||||
/>
|
||||
}
|
||||
menuEnd={
|
||||
onOpenProfile && (
|
||||
<MenuItem
|
||||
Icon={UserProfileIcon}
|
||||
label={t("common.profile")}
|
||||
onSelect={onOpenProfile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
menuStart={menuStart}
|
||||
menuEnd={menuEnd}
|
||||
focusable={focusable}
|
||||
focusUrl={focusUrl}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user