mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-06 18:19:20 +00:00
WIP: Add app bar and earpiece toggle button
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user