mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-19 06:20:25 +00:00
Add an earpiece overlay
This commit is contained in:
@@ -79,6 +79,10 @@
|
||||
"use_to_device_key_transport": "Use to device key transport. This will fallback to room key transport when another call member sent a room key"
|
||||
},
|
||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||
"earpiece": {
|
||||
"overlay_title": "Earpiece only mode",
|
||||
"overlay_description": "Only works while using app"
|
||||
},
|
||||
"error": {
|
||||
"call_is_not_supported": "Call is not supported",
|
||||
"call_not_found": "Call not found",
|
||||
|
||||
63
src/room/EarpieceOverlay.module.css
Normal file
63
src/room/EarpieceOverlay.module.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
z-index: var(--overlay-layer);
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--cpd-space-2x);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay[data-show="true"] {
|
||||
animation: fade-in 200ms;
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay[data-show="false"] {
|
||||
animation: fade-out 130ms forwards;
|
||||
content-visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.overlay::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
background: var(--cpd-color-bg-canvas-default);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-block-end: var(--cpd-space-4x);
|
||||
background: var(--cpd-color-alpha-gray-600);
|
||||
color: var(--cpd-color-icon-primary);
|
||||
}
|
||||
|
||||
.overlay > h2 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.overlay > p {
|
||||
text-align: center;
|
||||
}
|
||||
32
src/room/EarpieceOverlay.tsx
Normal file
32
src/room/EarpieceOverlay.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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 { type FC } from "react";
|
||||
import { BigIcon, Heading, Text } from "@vector-im/compound-web";
|
||||
import { EarpieceIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./EarpieceOverlay.module.css";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const EarpieceOverlay: FC<Props> = ({ show }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={styles.overlay} data-show={show}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<EarpieceIcon aria-hidden />
|
||||
</BigIcon>
|
||||
<Heading as="h2" weight="semibold" size="md">
|
||||
{t("earpiece.overlay_title")}
|
||||
</Heading>
|
||||
<Text>{t("earpiece.overlay_description")}</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,13 +12,15 @@ 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: 1;
|
||||
z-index: var(--header-footer-layer);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
@@ -34,7 +36,7 @@ Please see LICENSE in the repository root for full details.
|
||||
.footer {
|
||||
position: sticky;
|
||||
inset-block-end: 0;
|
||||
z-index: 1;
|
||||
z-index: var(--header-footer-layer);
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(0, var(--inline-content-inset))
|
||||
|
||||
@@ -105,6 +105,8 @@ import { useTypedEventEmitter } from "../useEvents.ts";
|
||||
import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
|
||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships.ts";
|
||||
import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
|
||||
@@ -116,6 +118,7 @@ export interface ActiveCallProps
|
||||
}
|
||||
|
||||
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
const mediaDevices = useMediaDevices();
|
||||
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
||||
const { livekitRoom, connState } = useLivekit(
|
||||
props.rtcSession,
|
||||
@@ -156,6 +159,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
const vm = new CallViewModel(
|
||||
props.rtcSession,
|
||||
livekitRoom,
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
connStateObservable$,
|
||||
reactionsReader.raisedHands$,
|
||||
@@ -167,7 +171,13 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
reactionsReader.destroy();
|
||||
};
|
||||
}
|
||||
}, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable$]);
|
||||
}, [
|
||||
props.rtcSession,
|
||||
livekitRoom,
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
connStateObservable$,
|
||||
]);
|
||||
|
||||
if (livekitRoom === undefined || vm === null) return null;
|
||||
|
||||
@@ -293,6 +303,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const gridMode = useObservableEagerState(vm.gridMode$);
|
||||
const showHeader = useObservableEagerState(vm.showHeader$);
|
||||
const showFooter = useObservableEagerState(vm.showFooter$);
|
||||
const earpieceMode = useObservableEagerState(vm.earpieceMode$);
|
||||
const switchCamera = useSwitchCamera(vm.localVideo$);
|
||||
|
||||
// Ideally we could detect taps by listening for click events and checking
|
||||
@@ -728,6 +739,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{renderContent()}
|
||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<EarpieceOverlay show={earpieceMode} />
|
||||
<ReactionsOverlay vm={vm} />
|
||||
{footer}
|
||||
{layout.type !== "pip" && (
|
||||
|
||||
@@ -35,6 +35,41 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="overlay"
|
||||
data-show="false"
|
||||
>
|
||||
<div
|
||||
class="_content_o77nw_8 icon"
|
||||
data-size="large"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14 2c3.93 0 7 3.07 7 7a1 1 0 0 1-2 0c0-2.8-2.2-5-5-5S9 6.2 9 9c0 .93.29 1.98.82 2.94.71 1.29 1.53 1.92 2.32 2.53.92.71 1.88 1.44 2.39 3 .5 1.5 1 2.01 1.71 2.38.2.09.47.15.76.15 1.1 0 2-.9 2-2a1 1 0 1 1 2 0 4 4 0 0 1-5.64 3.65c-1.36-.71-2.13-1.73-2.73-3.55-.32-.98-.9-1.43-1.71-2.05-.87-.67-1.94-1.5-2.85-3.15C7.38 11.65 7 10.26 7 9c0-3.93 3.07-7 7-7"
|
||||
/>
|
||||
<path
|
||||
d="M6.145 1.3a1 1 0 0 1 1.427 1.4A8.97 8.97 0 0 0 5 9c0 2.3.862 4.397 2.281 5.988l.291.312.069.077A1 1 0 0 1 6.22 16.77l-.075-.07-.356-.38A10.96 10.96 0 0 1 3 9c0-2.998 1.2-5.717 3.145-7.7M14 6.5a2.5 2.5 0 0 1 0 5 2.501 2.501 0 0 1 0-5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Earpiece only mode
|
||||
</h2>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
|
||||
>
|
||||
Only works while using app
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="container"
|
||||
/>
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
localId,
|
||||
localRtcMember,
|
||||
} from "../utils/test-fixtures";
|
||||
import { type MediaDevices } from "./MediaDevices";
|
||||
|
||||
vi.mock("@livekit/components-core");
|
||||
|
||||
@@ -262,6 +263,7 @@ function withCallViewModel(
|
||||
const vm = new CallViewModel(
|
||||
rtcSession as unknown as MatrixRTCSession,
|
||||
liveKitRoom,
|
||||
{} as unknown as MediaDevices,
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
import { observeSpeaker$ } from "./observeSpeaker";
|
||||
import { shallowEquals } from "../utils/array";
|
||||
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
||||
import { type MediaDevices } from "./MediaDevices";
|
||||
|
||||
// How long we wait after a focus switch before showing the real participant
|
||||
// list again
|
||||
@@ -1246,6 +1247,11 @@ export class CallViewModel extends ViewModel {
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether audio is currently being output through the earpiece.
|
||||
*/
|
||||
public readonly earpieceMode$ = this.mediaDevices.earpieceMode$;
|
||||
|
||||
public readonly reactions$ = this.reactionsSubject$.pipe(
|
||||
map((v) =>
|
||||
Object.fromEntries(
|
||||
@@ -1336,6 +1342,7 @@ export class CallViewModel extends ViewModel {
|
||||
// A call is permanently tied to a single Matrix room and LiveKit room
|
||||
private readonly matrixRTCSession: MatrixRTCSession,
|
||||
private readonly livekitRoom: LivekitRoom,
|
||||
private readonly mediaDevices: MediaDevices,
|
||||
private readonly encryptionSystem: EncryptionSystem,
|
||||
private readonly connectionState$: Observable<ECConnectionState>,
|
||||
private readonly handsRaisedSubject$: Observable<
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
localRtcMember,
|
||||
} from "./test-fixtures";
|
||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||
import { type MediaDevices } from "../state/MediaDevices";
|
||||
|
||||
export function getBasicRTCSession(
|
||||
members: RoomMember[],
|
||||
@@ -132,6 +133,7 @@ export function getBasicCallViewModelEnvironment(
|
||||
const vm = new CallViewModel(
|
||||
rtcSession as unknown as MatrixRTCSession,
|
||||
liveKitRoom,
|
||||
{ earpieceMode$: of(false) } as unknown as MediaDevices,
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user