WIP: Add app bar and earpiece toggle button

This commit is contained in:
Robin
2025-06-23 21:54:48 -04:00
committed by Timo
parent 6383c94f2f
commit e112b527a8
20 changed files with 380 additions and 150 deletions

View File

@@ -25,6 +25,7 @@ import { LinkButton } from "../button";
interface Props {
client: MatrixClient;
isPasswordlessUser: boolean;
hideHeader: boolean;
confineToRoom: boolean;
endedCallId: string;
}
@@ -32,6 +33,7 @@ interface Props {
export const CallEndedView: FC<Props> = ({
client,
isPasswordlessUser,
hideHeader,
confineToRoom,
endedCallId,
}) => {
@@ -133,10 +135,12 @@ export const CallEndedView: FC<Props> = ({
return (
<>
<Header>
<LeftNav>{!confineToRoom && <HeaderLogo />}</LeftNav>
<RightNav />
</Header>
{!hideHeader && (
<Header>
<LeftNav>{!confineToRoom && <HeaderLogo />}</LeftNav>
<RightNav />
</Header>
)}
<div className={styles.container}>
<main className={styles.main}>
<Heading size="xl" weight="semibold" className={styles.headline}>

View File

@@ -1,6 +1,6 @@
.overlay {
position: fixed;
z-index: var(--overlay-layer);
z-index: var(--call-view-overlay-layer);
inset: 0;
display: flex;
flex-direction: column;

View File

@@ -72,6 +72,7 @@ import {
} from "../settings/settings";
import { useTypedEventEmitter } from "../useEvents";
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { useAppBarTitle } from "../AppBar.tsx";
declare global {
interface Window {
@@ -177,6 +178,7 @@ export const GroupCallView: FC<Props> = ({
}, [passwordFromUrl, room.roomId]);
usePageTitle(roomName);
useAppBarTitle(roomName);
const matrixInfo = useMemo((): MatrixInfo => {
return {
@@ -473,6 +475,7 @@ export const GroupCallView: FC<Props> = ({
endedCallId={rtcSession.room.roomId}
client={client}
isPasswordlessUser={isPasswordlessUser}
hideHeader={hideHeader}
confineToRoom={confineToRoom}
/>
);

View File

@@ -12,15 +12,13 @@ Please see LICENSE in the repository root for full details.
width: 100%;
overflow-x: hidden;
overflow-y: auto;
--overlay-layer: 1;
--header-footer-layer: 2;
}
.header {
position: sticky;
flex-shrink: 0;
inset-block-start: 0;
z-index: var(--header-footer-layer);
z-index: var(--call-view-header-footer-layer);
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0) 0%,
@@ -36,7 +34,7 @@ Please see LICENSE in the repository root for full details.
.footer {
position: sticky;
inset-block-end: 0;
z-index: var(--header-footer-layer);
z-index: var(--call-view-header-footer-layer);
display: grid;
grid-template-columns:
minmax(0, var(--inline-content-inset))

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
import { Text } from "@vector-im/compound-web";
import { IconButton, Text, Tooltip } from "@vector-im/compound-web";
import { ConnectionState, type Room } from "livekit-client";
import { type MatrixClient } from "matrix-js-sdk";
import {
@@ -21,6 +21,7 @@ import {
useRef,
useState,
type JSX,
type ReactNode,
} from "react";
import useMeasure from "react-use-measure";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
@@ -29,6 +30,10 @@ import { BehaviorSubject, map } from "rxjs";
import { useObservable, useObservableEagerState } from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger";
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
import {
EarpieceIcon,
VolumeOnSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import LogoMark from "../icons/LogoMark.svg?react";
import LogoType from "../icons/LogoType.svg?react";
@@ -107,6 +112,8 @@ import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships.ts";
import { useMediaDevices } from "../MediaDevicesContext.ts";
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
import { useAppBarSecondaryButton } from "../AppBar.tsx";
import { useTranslation } from "react-i18next";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -205,7 +212,7 @@ export interface InCallViewProps {
participantCount: number;
/** Function to call when the user explicitly ends the call */
onLeave: () => void;
hideHeader: boolean;
header: "none" | "standard" | "app_bar";
otelGroupCallMembership?: OTelGroupCallMembership;
connState: ECConnectionState;
onShareClick: (() => void) | null;
@@ -220,10 +227,11 @@ export const InCallView: FC<InCallViewProps> = ({
muteStates,
participantCount,
onLeave,
hideHeader,
header: headerStyle,
connState,
onShareClick,
}) => {
const { t } = useTranslation();
const { supportsReactions, sendReaction, toggleRaisedHand } =
useReactionsSender();
@@ -304,6 +312,7 @@ export const InCallView: FC<InCallViewProps> = ({
const showHeader = useObservableEagerState(vm.showHeader$);
const showFooter = useObservableEagerState(vm.showFooter$);
const earpieceMode = useObservableEagerState(vm.earpieceMode$);
const toggleEarpieceMode = useObservableEagerState(vm.toggleEarpieceMode$);
const switchCamera = useSwitchCamera(vm.localVideo$);
// Ideally we could detect taps by listening for click events and checking
@@ -446,6 +455,69 @@ export const InCallView: FC<InCallViewProps> = ({
}
}, [setGridMode]);
useAppBarSecondaryButton(
useMemo(() => {
if (toggleEarpieceMode === null) return null;
const Icon = earpieceMode ? EarpieceIcon : VolumeOnSolidIcon;
return (
<Tooltip
label={
earpieceMode
? t("settings.devices.earpiece")
: t("settings.devices.loudspeaker")
}
>
<IconButton
onClick={(e) => {
e.preventDefault();
toggleEarpieceMode();
}}
>
<Icon />
</IconButton>
</Tooltip>
);
}, [t, earpieceMode, toggleEarpieceMode]),
);
let header: ReactNode = null;
if (showHeader) {
switch (headerStyle) {
case "none":
// Cosmetic header to fill out space while still affecting the bounds of
// the grid
header = (
<div
className={classNames(styles.header, styles.filler)}
ref={headerRef}
/>
);
break;
case "standard":
header = (
<Header className={styles.header} ref={headerRef}>
<LeftNav>
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
participantCount={participantCount}
/>
</LeftNav>
<RightNav>
{showControls && onShareClick !== null && (
<InviteButton
className={styles.invite}
onClick={onShareClick}
/>
)}
</RightNav>
</Header>
);
}
}
const Tile = useMemo(
() =>
forwardRef<
@@ -532,7 +604,8 @@ export const InCallView: FC<InCallViewProps> = ({
key="fixed"
className={styles.fixedGrid}
style={{
insetBlockStart: headerBounds.bottom,
insetBlockStart:
headerBounds.height > 0 ? headerBounds.bottom : bounds.top,
height: gridBounds.height,
}}
model={layout}
@@ -655,10 +728,11 @@ export const InCallView: FC<InCallViewProps> = ({
ref={footerRef}
className={classNames(styles.footer, {
[styles.overlay]: windowMode === "flat",
[styles.hidden]: !showFooter || (!showControls && hideHeader),
[styles.hidden]:
!showFooter || (!showControls && headerStyle === "none"),
})}
>
{!hideHeader && (
{headerStyle !== "none" && (
<div className={styles.logo}>
<LogoMark width={24} height={24} aria-hidden />
<LogoType
@@ -694,35 +768,7 @@ export const InCallView: FC<InCallViewProps> = ({
onPointerMove={onPointerMove}
onPointerOut={onPointerOut}
>
{showHeader &&
(hideHeader ? (
// Cosmetic header to fill out space while still affecting the bounds
// of the grid
<div
className={classNames(styles.header, styles.filler)}
ref={headerRef}
/>
) : (
<Header className={styles.header} ref={headerRef}>
<LeftNav>
<RoomHeaderInfo
id={matrixInfo.roomId}
name={matrixInfo.roomName}
avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
participantCount={participantCount}
/>
</LeftNav>
<RightNav>
{showControls && onShareClick !== null && (
<InviteButton
className={styles.invite}
onClick={onShareClick}
/>
)}
</RightNav>
</Header>
))}
{header}
{
// TODO: remove this once we remove the developer flag gets removed and we have shipped to
// device transport as the default.

View File

@@ -19,8 +19,10 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
import { Config } from "../config/Config";
import { ExternalLink, Link } from "../button/Link";
import { useUrlParams } from "../UrlParams";
export const RoomAuthView: FC = () => {
const { header } = useUrlParams();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
@@ -53,14 +55,16 @@ export const RoomAuthView: FC = () => {
return (
<>
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav>
<UserMenuContainer preventNavigation />
</RightNav>
</Header>
{header === "standard" && (
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav>
<UserMenuContainer preventNavigation />
</RightNav>
</Header>
)}
<div className={styles.container}>
<main className={styles.main}>
<Heading size="xl" weight="semibold" className={styles.headline}>

View File

@@ -43,14 +43,8 @@ import { ErrorView } from "../ErrorView";
import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState";
export const RoomPage: FC = () => {
const {
confineToRoom,
appPrompt,
preload,
hideHeader,
displayName,
skipLobby,
} = useUrlParams();
const { confineToRoom, appPrompt, preload, header, displayName, skipLobby } =
useUrlParams();
const { t } = useTranslation();
const { roomAlias, roomId, viaServers } = useRoomIdentifier();
@@ -120,7 +114,7 @@ export const RoomPage: FC = () => {
confineToRoom={confineToRoom}
preload={preload}
skipLobby={skipLobby || wasInWaitForInviteState.current}
hideHeader={hideHeader}
hideHeader={header !== "standard"}
muteStates={muteStates}
/>
);
@@ -161,7 +155,7 @@ export const RoomPage: FC = () => {
enterLabel={label}
waitingForInvite={groupCallState.kind === "waitForInvite"}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
hideHeader={header !== "standard"}
participantCount={null}
muteStates={muteStates}
onShareClick={null}