mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-10 05:57:07 +00:00
Merge branch 'robin/switch-camera-tile' into robin/reactions-small
This commit is contained in:
@@ -60,7 +60,7 @@ export function CallEventAudioRenderer({
|
||||
const audioEngineRef = useLatest(audioEngineCtx);
|
||||
|
||||
useEffect(() => {
|
||||
const joinSub = vm.memberChanges$
|
||||
const joinSub = vm.participantChanges$
|
||||
.pipe(
|
||||
filter(
|
||||
({ joined, ids }) =>
|
||||
@@ -72,7 +72,7 @@ export function CallEventAudioRenderer({
|
||||
void audioEngineRef.current?.playSound("join");
|
||||
});
|
||||
|
||||
const leftSub = vm.memberChanges$
|
||||
const leftSub = vm.participantChanges$
|
||||
.pipe(
|
||||
filter(
|
||||
({ ids, left }) =>
|
||||
|
||||
@@ -61,3 +61,7 @@
|
||||
.overlay > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: var(--cpd-space-32x);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { type FC } from "react";
|
||||
import { BigIcon, Button, Heading, Text } from "@vector-im/compound-web";
|
||||
import { EarpieceIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { VoiceCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./EarpieceOverlay.module.css";
|
||||
@@ -22,12 +22,12 @@ export const EarpieceOverlay: FC<Props> = ({ show, onBackToVideoPressed }) => {
|
||||
return (
|
||||
<div className={styles.overlay} data-show={show}>
|
||||
<BigIcon className={styles.icon}>
|
||||
<EarpieceIcon aria-hidden />
|
||||
<VoiceCallIcon aria-hidden />
|
||||
</BigIcon>
|
||||
<Heading as="h2" weight="semibold" size="md">
|
||||
{t("earpiece.overlay_title")}
|
||||
{t("handset.overlay_title")}
|
||||
</Heading>
|
||||
<Text>{t("earpiece.overlay_description")}</Text>
|
||||
<Text>{t("handset.overlay_description")}</Text>
|
||||
<Button
|
||||
kind="primary"
|
||||
size="sm"
|
||||
@@ -35,8 +35,10 @@ export const EarpieceOverlay: FC<Props> = ({ show, onBackToVideoPressed }) => {
|
||||
onBackToVideoPressed?.();
|
||||
}}
|
||||
>
|
||||
{t("earpiece.overlay_back_button")}
|
||||
{t("handset.overlay_back_button")}
|
||||
</Button>
|
||||
{/* This spacer is used to give the overlay an offset to the top. */}
|
||||
<div className={styles.spacer} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
OfflineIcon,
|
||||
WebBrowserIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import {
|
||||
ConnectionLostError,
|
||||
@@ -93,9 +94,13 @@ const ErrorPage: FC<ErrorPageProps> = ({
|
||||
</p>
|
||||
{actions &&
|
||||
actions.map((action, index) => (
|
||||
<button onClick={action.onClick} key={`action${index}`}>
|
||||
<Button
|
||||
kind="secondary"
|
||||
onClick={action.onClick}
|
||||
key={`action${index}`}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</ErrorView>
|
||||
</FullScreenView>
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { render, waitFor, screen } from "@testing-library/react";
|
||||
import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { of } from "rxjs";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
|
||||
@@ -43,6 +42,7 @@ import { MatrixRTCFocusMissingError } from "../utils/errors";
|
||||
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
|
||||
import { MediaDevicesContext } from "../MediaDevicesContext";
|
||||
import { HeaderStyle } from "../UrlParams";
|
||||
import { constant } from "../state/Behavior";
|
||||
|
||||
vi.mock("../soundUtils");
|
||||
vi.mock("../useAudioContext");
|
||||
@@ -141,7 +141,7 @@ function createGroupCallView(
|
||||
room,
|
||||
localRtcMember,
|
||||
[],
|
||||
).withMemberships(of([]));
|
||||
).withMemberships(constant([]));
|
||||
rtcSession.joined = joined;
|
||||
const muteState = {
|
||||
audio: { enabled: false },
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
|
||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import {
|
||||
@@ -72,6 +71,7 @@ import {
|
||||
import { useTypedEventEmitter } from "../useEvents";
|
||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||
import { useAppBarTitle } from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -110,7 +110,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
);
|
||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||
|
||||
const muteAllAudio = useObservableEagerState(muteAllAudio$);
|
||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||
const leaveSoundContext = useLatest(
|
||||
useAudioContext({
|
||||
sounds: callEventAudioSounds,
|
||||
@@ -166,7 +166,11 @@ export const GroupCallView: FC<Props> = ({
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const roomName = useRoomName(room);
|
||||
const roomAvatar = useRoomAvatar(room);
|
||||
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
||||
const {
|
||||
perParticipantE2EE,
|
||||
returnToLobby,
|
||||
password: passwordFromUrl,
|
||||
} = useUrlParams();
|
||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
|
||||
const [useExperimentalToDeviceTransport] = useSetting(
|
||||
@@ -174,7 +178,6 @@ export const GroupCallView: FC<Props> = ({
|
||||
);
|
||||
|
||||
// Save the password once we start the groupCallView
|
||||
const { password: passwordFromUrl } = useUrlParams();
|
||||
useEffect(() => {
|
||||
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
|
||||
}, [passwordFromUrl, room.roomId]);
|
||||
|
||||
@@ -25,11 +25,11 @@ import useMeasure from "react-use-measure";
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import classNames from "classnames";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
import { useObservable, useObservableEagerState } from "observable-hooks";
|
||||
import { useObservable, useSubscription } from "observable-hooks";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
|
||||
import {
|
||||
EarpieceIcon,
|
||||
VoiceCallSolidIcon,
|
||||
VolumeOnSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -110,6 +110,7 @@ import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMembership
|
||||
import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||
import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
|
||||
@@ -137,17 +138,17 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
logger.info(
|
||||
`[Lifecycle] InCallView Component mounted, livekitroom state ${livekitRoom?.state}`,
|
||||
`[Lifecycle] InCallView Component mounted, livekit room state ${livekitRoom?.state}`,
|
||||
);
|
||||
return (): void => {
|
||||
logger.info(
|
||||
`[Lifecycle] InCallView Component unmounted, livekitroom state ${livekitRoom?.state}`,
|
||||
`[Lifecycle] InCallView Component unmounted, livekit room state ${livekitRoom?.state}`,
|
||||
);
|
||||
livekitRoom
|
||||
?.disconnect()
|
||||
.then(() => {
|
||||
logger.info(
|
||||
`[Lifecycle] Disconnected from livekite room, state:${livekitRoom?.state}`,
|
||||
`[Lifecycle] Disconnected from livekit room, state:${livekitRoom?.state}`,
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -156,6 +157,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
};
|
||||
}, [livekitRoom]);
|
||||
|
||||
const { autoLeaveWhenOthersLeft } = useUrlParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (livekitRoom !== undefined) {
|
||||
const reactionsReader = new ReactionsReader(props.rtcSession);
|
||||
@@ -163,7 +166,10 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
props.rtcSession,
|
||||
livekitRoom,
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
{
|
||||
encryptionSystem: props.e2eeSystem,
|
||||
autoLeaveWhenOthersLeft,
|
||||
},
|
||||
connStateObservable$,
|
||||
reactionsReader.raisedHands$,
|
||||
reactionsReader.reactions$,
|
||||
@@ -180,6 +186,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
mediaDevices,
|
||||
props.e2eeSystem,
|
||||
connStateObservable$,
|
||||
autoLeaveWhenOthersLeft,
|
||||
]);
|
||||
|
||||
if (livekitRoom === undefined || vm === null) return null;
|
||||
@@ -249,7 +256,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
room: livekitRoom,
|
||||
});
|
||||
|
||||
const muteAllAudio = useObservableEagerState(muteAllAudio$);
|
||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||
|
||||
// This seems like it might be enough logic to use move it into the call view model?
|
||||
const [didFallbackToRoomKey, setDidFallbackToRoomKey] = useState(false);
|
||||
@@ -300,15 +307,16 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => void toggleRaisedHand(),
|
||||
);
|
||||
|
||||
const windowMode = useObservableEagerState(vm.windowMode$);
|
||||
const layout = useObservableEagerState(vm.layout$);
|
||||
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration$);
|
||||
const windowMode = useBehavior(vm.windowMode$);
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$);
|
||||
const [debugTileLayout] = useSetting(debugTileLayoutSetting);
|
||||
const gridMode = useObservableEagerState(vm.gridMode$);
|
||||
const showHeader = useObservableEagerState(vm.showHeader$);
|
||||
const showFooter = useObservableEagerState(vm.showFooter$);
|
||||
const earpieceMode = useObservableEagerState(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useObservableEagerState(vm.audioOutputSwitcher$);
|
||||
const gridMode = useBehavior(vm.gridMode$);
|
||||
const showHeader = useBehavior(vm.showHeader$);
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||
useSubscription(vm.autoLeaveWhenOthersLeft$, onLeave);
|
||||
|
||||
// Ideally we could detect taps by listening for click events and checking
|
||||
// that the pointerType of the event is "touch", but this isn't yet supported
|
||||
@@ -454,9 +462,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
useMemo(() => {
|
||||
if (audioOutputSwitcher === null) return null;
|
||||
const isEarpieceTarget = audioOutputSwitcher.targetOutput === "earpiece";
|
||||
const Icon = isEarpieceTarget ? EarpieceIcon : VolumeOnSolidIcon;
|
||||
const Icon = isEarpieceTarget ? VoiceCallSolidIcon : VolumeOnSolidIcon;
|
||||
const label = isEarpieceTarget
|
||||
? t("settings.devices.earpiece")
|
||||
? t("settings.devices.handset")
|
||||
: t("settings.devices.loudspeaker");
|
||||
|
||||
return (
|
||||
@@ -524,16 +532,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
targetHeight,
|
||||
model,
|
||||
}: TileProps<TileViewModel, HTMLDivElement>): ReactNode {
|
||||
const spotlightExpanded = useObservableEagerState(
|
||||
vm.spotlightExpanded$,
|
||||
);
|
||||
const onToggleExpanded = useObservableEagerState(
|
||||
vm.toggleSpotlightExpanded$,
|
||||
);
|
||||
const showSpeakingIndicatorsValue = useObservableEagerState(
|
||||
const spotlightExpanded = useBehavior(vm.spotlightExpanded$);
|
||||
const onToggleExpanded = useBehavior(vm.toggleSpotlightExpanded$);
|
||||
const showSpeakingIndicatorsValue = useBehavior(
|
||||
vm.showSpeakingIndicators$,
|
||||
);
|
||||
const showSpotlightIndicatorsValue = useObservableEagerState(
|
||||
const showSpotlightIndicatorsValue = useBehavior(
|
||||
vm.showSpotlightIndicators$,
|
||||
);
|
||||
|
||||
|
||||
@@ -191,7 +191,11 @@ describe("useMuteStates", () => {
|
||||
mockConfig();
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/room/?skipLobby=true"]}>
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
"/room/?skipLobby=true&widgetId=1234&parentUrl=www.parent.org",
|
||||
]}
|
||||
>
|
||||
<MediaDevicesContext value={mockMediaDevices()}>
|
||||
<TestComponent />
|
||||
</MediaDevicesContext>
|
||||
|
||||
@@ -86,6 +86,14 @@ export function useMuteStates(isJoined: boolean): MuteStates {
|
||||
const audio = useMuteState(devices.audioInput, () => {
|
||||
return Config.get().media_devices.enable_audio && !skipLobby && !isJoined;
|
||||
});
|
||||
useEffect(() => {
|
||||
// If audio is enabled, we need to request the device names again,
|
||||
// because iOS will not be able to switch to the correct device after un-muting.
|
||||
// This is one of the main changes that makes iOS work with bluetooth audio devices.
|
||||
if (audio.enabled) {
|
||||
devices.requestDeviceNames();
|
||||
}
|
||||
}, [audio.enabled, devices]);
|
||||
const isEarpiece = useIsEarpiece();
|
||||
const video = useMuteState(
|
||||
devices.videoInput,
|
||||
|
||||
@@ -6,16 +6,16 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
|
||||
import styles from "./ReactionsOverlay.module.css";
|
||||
import { type CallViewModel } from "../state/CallViewModel";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
|
||||
export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode {
|
||||
const reactionsIcons = useObservableState(vm.visibleReactions$);
|
||||
const reactionsIcons = useBehavior(vm.visibleReactions$);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{reactionsIcons?.map(({ sender, emoji, startX }) => (
|
||||
{reactionsIcons.map(({ sender, emoji, startX }) => (
|
||||
<span
|
||||
// Reactions effects are considered presentation elements. The reaction
|
||||
// is also present on the sender's tile, which assistive technology can
|
||||
|
||||
@@ -132,7 +132,13 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
|
||||
<p>
|
||||
You were disconnected from the call.
|
||||
</p>
|
||||
<button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Reconnect
|
||||
</button>
|
||||
<button
|
||||
@@ -742,7 +748,13 @@ exports[`should report correct error for 'Connection lost' 1`] = `
|
||||
<p>
|
||||
You were disconnected from the call.
|
||||
</p>
|
||||
<button>
|
||||
<button
|
||||
class="_button_vczzf_8"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Reconnect
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -98,18 +98,30 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
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"
|
||||
/>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M8.929 15.1a13.6 13.6 0 0 0 4.654 3.066q2.62 1.036 5.492.923h.008l.003-.004.003-.002-.034-3.124-3.52-.483-1.791 1.792-.645-.322a13.5 13.5 0 0 1-3.496-2.52 13.4 13.4 0 0 1-2.52-3.496l-.322-.644 1.792-1.792-.483-3.519-3.123-.034-.003.002-.003.004v.002a13.65 13.65 0 0 0 .932 5.492A13.4 13.4 0 0 0 8.93 15.1m3.92 4.926a15.6 15.6 0 0 1-5.334-3.511 15.4 15.4 0 0 1-3.505-5.346 15.6 15.6 0 0 1-1.069-6.274 1.93 1.93 0 0 1 .589-1.366c.366-.366.84-.589 1.386-.589h.01l3.163.035a1.96 1.96 0 0 1 1.958 1.694v.005l.487 3.545v.003c.043.297.025.605-.076.907a2 2 0 0 1-.485.773l-.762.762a11.4 11.4 0 0 0 3.206 3.54q.457.33.948.614l.762-.761a2 2 0 0 1 .774-.486c.302-.1.61-.118.907-.076l3.553.487a1.96 1.96 0 0 1 1.694 1.958l.034 3.174c0 .546-.223 1.02-.588 1.386-.361.36-.827.582-1.363.588a15.3 15.3 0 0 1-6.29-1.062"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<h2
|
||||
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
|
||||
>
|
||||
Earpiece Mode
|
||||
Handset Mode
|
||||
</h2>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
|
||||
@@ -125,6 +137,9 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
Back to Speaker Mode
|
||||
</button>
|
||||
<div
|
||||
class="spacer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="container"
|
||||
|
||||
Reference in New Issue
Block a user