mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +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"
|
"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.",
|
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||||
|
"earpiece": {
|
||||||
|
"overlay_title": "Earpiece only mode",
|
||||||
|
"overlay_description": "Only works while using app"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"call_is_not_supported": "Call is not supported",
|
"call_is_not_supported": "Call is not supported",
|
||||||
"call_not_found": "Call not found",
|
"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%;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
--overlay-layer: 1;
|
||||||
|
--header-footer-layer: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
z-index: 1;
|
z-index: var(--header-footer-layer);
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
0deg,
|
0deg,
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
@@ -34,7 +36,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
.footer {
|
.footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
inset-block-end: 0;
|
inset-block-end: 0;
|
||||||
z-index: 1;
|
z-index: var(--header-footer-layer);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
minmax(0, var(--inline-content-inset))
|
minmax(0, var(--inline-content-inset))
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ import { useTypedEventEmitter } from "../useEvents.ts";
|
|||||||
import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
|
import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
|
||||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships.ts";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships.ts";
|
||||||
|
import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||||
|
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ export interface ActiveCallProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||||
|
const mediaDevices = useMediaDevices();
|
||||||
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
const sfuConfig = useOpenIDSFU(props.client, props.rtcSession);
|
||||||
const { livekitRoom, connState } = useLivekit(
|
const { livekitRoom, connState } = useLivekit(
|
||||||
props.rtcSession,
|
props.rtcSession,
|
||||||
@@ -156,6 +159,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
props.rtcSession,
|
props.rtcSession,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
|
mediaDevices,
|
||||||
props.e2eeSystem,
|
props.e2eeSystem,
|
||||||
connStateObservable$,
|
connStateObservable$,
|
||||||
reactionsReader.raisedHands$,
|
reactionsReader.raisedHands$,
|
||||||
@@ -167,7 +171,13 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
reactionsReader.destroy();
|
reactionsReader.destroy();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable$]);
|
}, [
|
||||||
|
props.rtcSession,
|
||||||
|
livekitRoom,
|
||||||
|
mediaDevices,
|
||||||
|
props.e2eeSystem,
|
||||||
|
connStateObservable$,
|
||||||
|
]);
|
||||||
|
|
||||||
if (livekitRoom === undefined || vm === null) return null;
|
if (livekitRoom === undefined || vm === null) return null;
|
||||||
|
|
||||||
@@ -293,6 +303,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
const gridMode = useObservableEagerState(vm.gridMode$);
|
const gridMode = useObservableEagerState(vm.gridMode$);
|
||||||
const showHeader = useObservableEagerState(vm.showHeader$);
|
const showHeader = useObservableEagerState(vm.showHeader$);
|
||||||
const showFooter = useObservableEagerState(vm.showFooter$);
|
const showFooter = useObservableEagerState(vm.showFooter$);
|
||||||
|
const earpieceMode = useObservableEagerState(vm.earpieceMode$);
|
||||||
const switchCamera = useSwitchCamera(vm.localVideo$);
|
const switchCamera = useSwitchCamera(vm.localVideo$);
|
||||||
|
|
||||||
// Ideally we could detect taps by listening for click events and checking
|
// Ideally we could detect taps by listening for click events and checking
|
||||||
@@ -728,6 +739,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
{renderContent()}
|
{renderContent()}
|
||||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||||
|
<EarpieceOverlay show={earpieceMode} />
|
||||||
<ReactionsOverlay vm={vm} />
|
<ReactionsOverlay vm={vm} />
|
||||||
{footer}
|
{footer}
|
||||||
{layout.type !== "pip" && (
|
{layout.type !== "pip" && (
|
||||||
|
|||||||
@@ -35,6 +35,41 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</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
|
<div
|
||||||
class="container"
|
class="container"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import {
|
|||||||
localId,
|
localId,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
} from "../utils/test-fixtures";
|
} from "../utils/test-fixtures";
|
||||||
|
import { type MediaDevices } from "./MediaDevices";
|
||||||
|
|
||||||
vi.mock("@livekit/components-core");
|
vi.mock("@livekit/components-core");
|
||||||
|
|
||||||
@@ -262,6 +263,7 @@ function withCallViewModel(
|
|||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
rtcSession as unknown as MatrixRTCSession,
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
|
{} as unknown as MediaDevices,
|
||||||
{
|
{
|
||||||
kind: E2eeType.PER_PARTICIPANT,
|
kind: E2eeType.PER_PARTICIPANT,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import {
|
|||||||
import { observeSpeaker$ } from "./observeSpeaker";
|
import { observeSpeaker$ } from "./observeSpeaker";
|
||||||
import { shallowEquals } from "../utils/array";
|
import { shallowEquals } from "../utils/array";
|
||||||
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
||||||
|
import { type MediaDevices } from "./MediaDevices";
|
||||||
|
|
||||||
// How long we wait after a focus switch before showing the real participant
|
// How long we wait after a focus switch before showing the real participant
|
||||||
// list again
|
// list again
|
||||||
@@ -1246,6 +1247,11 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether audio is currently being output through the earpiece.
|
||||||
|
*/
|
||||||
|
public readonly earpieceMode$ = this.mediaDevices.earpieceMode$;
|
||||||
|
|
||||||
public readonly reactions$ = this.reactionsSubject$.pipe(
|
public readonly reactions$ = this.reactionsSubject$.pipe(
|
||||||
map((v) =>
|
map((v) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
@@ -1336,6 +1342,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
// A call is permanently tied to a single Matrix room and LiveKit room
|
// A call is permanently tied to a single Matrix room and LiveKit room
|
||||||
private readonly matrixRTCSession: MatrixRTCSession,
|
private readonly matrixRTCSession: MatrixRTCSession,
|
||||||
private readonly livekitRoom: LivekitRoom,
|
private readonly livekitRoom: LivekitRoom,
|
||||||
|
private readonly mediaDevices: MediaDevices,
|
||||||
private readonly encryptionSystem: EncryptionSystem,
|
private readonly encryptionSystem: EncryptionSystem,
|
||||||
private readonly connectionState$: Observable<ECConnectionState>,
|
private readonly connectionState$: Observable<ECConnectionState>,
|
||||||
private readonly handsRaisedSubject$: Observable<
|
private readonly handsRaisedSubject$: Observable<
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
localRtcMember,
|
localRtcMember,
|
||||||
} from "./test-fixtures";
|
} from "./test-fixtures";
|
||||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||||
|
import { type MediaDevices } from "../state/MediaDevices";
|
||||||
|
|
||||||
export function getBasicRTCSession(
|
export function getBasicRTCSession(
|
||||||
members: RoomMember[],
|
members: RoomMember[],
|
||||||
@@ -132,6 +133,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
rtcSession as unknown as MatrixRTCSession,
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
|
{ earpieceMode$: of(false) } as unknown as MediaDevices,
|
||||||
{
|
{
|
||||||
kind: E2eeType.PER_PARTICIPANT,
|
kind: E2eeType.PER_PARTICIPANT,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user