mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-04 05:37:22 +00:00
Pause media tracks and show a message when reconnecting to MatrixRTC
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
"profile": "Profile",
|
||||
"reaction": "Reaction",
|
||||
"reactions": "Reactions",
|
||||
"reconnecting": "Reconnecting…",
|
||||
"settings": "Settings",
|
||||
"unencrypted": "Not encrypted",
|
||||
"username": "Username",
|
||||
|
||||
@@ -45,6 +45,12 @@ interface Props {
|
||||
* A supporting icon to display within the toast.
|
||||
*/
|
||||
Icon?: ComponentType<SVGAttributes<SVGElement>>;
|
||||
/**
|
||||
* Whether the toast should be portaled into the root of the document (rather
|
||||
* than rendered in-place within the component tree).
|
||||
* @default true
|
||||
*/
|
||||
portal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +62,7 @@ export const Toast: FC<Props> = ({
|
||||
autoDismiss,
|
||||
children,
|
||||
Icon,
|
||||
portal = true,
|
||||
}) => {
|
||||
const onOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
@@ -71,29 +78,33 @@ export const Toast: FC<Props> = ({
|
||||
}
|
||||
}, [open, autoDismiss, onDismiss]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<DialogOverlay
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
/>
|
||||
<DialogContent aria-describedby={undefined} asChild>
|
||||
<DialogClose
|
||||
className={classNames(
|
||||
overlayStyles.overlay,
|
||||
overlayStyles.animate,
|
||||
styles.toast,
|
||||
)}
|
||||
>
|
||||
<DialogTitle asChild>
|
||||
<Text as="h3" size="sm" weight="semibold">
|
||||
{children}
|
||||
</Text>
|
||||
</DialogTitle>
|
||||
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogRoot open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
className={classNames(overlayStyles.bg, overlayStyles.animate)}
|
||||
/>
|
||||
<DialogContent aria-describedby={undefined} asChild>
|
||||
<DialogClose
|
||||
className={classNames(
|
||||
overlayStyles.overlay,
|
||||
overlayStyles.animate,
|
||||
styles.toast,
|
||||
)}
|
||||
>
|
||||
<DialogTitle asChild>
|
||||
<Text as="h3" size="sm" weight="semibold">
|
||||
{children}
|
||||
</Text>
|
||||
</DialogTitle>
|
||||
{Icon && <Icon width={20} height={20} aria-hidden />}
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
{portal ? <DialogPortal>{content}</DialogPortal> : content}
|
||||
</DialogRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -113,6 +113,7 @@ import { useMediaDevices } from "../MediaDevicesContext.ts";
|
||||
import { EarpieceOverlay } from "./EarpieceOverlay.tsx";
|
||||
import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
import { Toast } from "../Toast.tsx";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
|
||||
@@ -313,6 +314,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => void toggleRaisedHand(),
|
||||
);
|
||||
|
||||
const reconnecting = useBehavior(vm.reconnecting$);
|
||||
const windowMode = useBehavior(vm.windowMode$);
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$);
|
||||
@@ -766,6 +768,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
// The reconnecting toast cannot be dismissed
|
||||
const onDismissReconnectingToast = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.inRoom}
|
||||
@@ -793,8 +798,15 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{renderContent()}
|
||||
<CallEventAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<ReactionsAudioRenderer vm={vm} muted={muteAllAudio} />
|
||||
<Toast
|
||||
onDismiss={onDismissReconnectingToast}
|
||||
open={reconnecting}
|
||||
portal={false}
|
||||
>
|
||||
{t("common.reconnecting")}
|
||||
</Toast>
|
||||
<EarpieceOverlay
|
||||
show={earpieceMode}
|
||||
show={earpieceMode && !reconnecting}
|
||||
onBackToVideoPressed={audioOutputSwitcher?.switch}
|
||||
/>
|
||||
<ReactionsOverlay vm={vm} />
|
||||
|
||||
@@ -256,7 +256,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-labelledby="«r5»"
|
||||
aria-labelledby="«r8»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -279,7 +279,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-labelledby="«ra»"
|
||||
aria-labelledby="«rd»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -301,7 +301,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="«rf»"
|
||||
aria-labelledby="«ri»"
|
||||
class="_button_vczzf_8 _has-icon_vczzf_57 _icon-only_vczzf_50"
|
||||
data-kind="secondary"
|
||||
data-size="lg"
|
||||
@@ -322,7 +322,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby="«rk»"
|
||||
aria-labelledby="«rn»"
|
||||
class="_button_vczzf_8 endCall _has-icon_vczzf_57 _icon-only_vczzf_50 _destructive_vczzf_107"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
@@ -348,7 +348,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
class="toggle layout"
|
||||
>
|
||||
<input
|
||||
aria-labelledby="«rp»"
|
||||
aria-labelledby="«rs»"
|
||||
name="layout"
|
||||
type="radio"
|
||||
value="spotlight"
|
||||
@@ -366,7 +366,7 @@ exports[`InCallView > rendering > renders 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
aria-labelledby="«ru»"
|
||||
aria-labelledby="«r11»"
|
||||
checked=""
|
||||
name="layout"
|
||||
type="radio"
|
||||
|
||||
@@ -394,6 +394,9 @@ function getRoomMemberFromRtcMember(
|
||||
|
||||
// TODO: Move wayyyy more business logic from the call and lobby views into here
|
||||
export class CallViewModel extends ViewModel {
|
||||
private readonly userId = this.matrixRoom.client.getUserId();
|
||||
private readonly deviceId = this.matrixRoom.client.getDeviceId();
|
||||
|
||||
public readonly localVideo$ = this.scope.behavior<LocalVideoTrack | null>(
|
||||
observeTrackReference$(
|
||||
this.livekitRoom.localParticipant,
|
||||
@@ -487,10 +490,33 @@ export class CallViewModel extends ViewModel {
|
||||
// Handle room membership changes (and displayname updates)
|
||||
fromEvent(this.matrixRoom, RoomStateEvent.Members),
|
||||
).pipe(
|
||||
startWith(this.matrixRTCSession.memberships),
|
||||
map(() => {
|
||||
return this.matrixRTCSession.memberships;
|
||||
}),
|
||||
startWith(null),
|
||||
map(() => this.matrixRTCSession.memberships),
|
||||
);
|
||||
|
||||
private readonly matrixRTCConnected$ = this.scope.behavior(
|
||||
this.memberships$.pipe(
|
||||
map((ms) =>
|
||||
ms.some(
|
||||
(m) => m.sender === this.userId && m.deviceId === this.deviceId,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
public readonly reconnecting$ = this.scope.behavior(
|
||||
this.matrixRTCConnected$.pipe(
|
||||
// We are reconnecting if we previously had some successful initial
|
||||
// connection but are now disconnected
|
||||
scan(
|
||||
({ connectedPreviously, reconnecting }, connectedNow) => ({
|
||||
connectedPreviously: connectedPreviously || connectedNow,
|
||||
reconnecting: connectedPreviously && !connectedNow,
|
||||
}),
|
||||
{ connectedPreviously: false, reconnecting: false },
|
||||
),
|
||||
map(({ reconnecting }) => reconnecting),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -787,12 +813,13 @@ export class CallViewModel extends ViewModel {
|
||||
|
||||
public readonly allOthersLeft$ = this.matrixUserChanges$.pipe(
|
||||
map(({ userIds, leftUserIds }) => {
|
||||
const userId = this.matrixRoom.client.getUserId();
|
||||
if (!userId) {
|
||||
logger.warn("Could access client.getUserId to compute allOthersLeft");
|
||||
if (!this.userId) {
|
||||
logger.warn("Could not access user ID to compute allOthersLeft");
|
||||
return false;
|
||||
}
|
||||
return userIds.size === 1 && userIds.has(userId) && leftUserIds.size > 0;
|
||||
return (
|
||||
userIds.size === 1 && userIds.has(this.userId) && leftUserIds.size > 0
|
||||
);
|
||||
}),
|
||||
startWith(false),
|
||||
distinctUntilChanged(),
|
||||
@@ -1502,5 +1529,44 @@ export class CallViewModel extends ViewModel {
|
||||
>,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Pause all media tracks when we're disconnected from MatrixRTC, because it
|
||||
// can be an unpleasant surprise for the app to say 'reconnecting' and yet
|
||||
// still be transmitting your media to others.
|
||||
this.matrixRTCConnected$.pipe(this.scope.bind()).subscribe((connected) => {
|
||||
const publications =
|
||||
this.livekitRoom.localParticipant.trackPublications.values();
|
||||
if (connected) {
|
||||
for (const p of publications) {
|
||||
if (p.track?.isUpstreamPaused === true) {
|
||||
const kind = p.track.kind;
|
||||
logger.log(`Reconnected to MatrixRTC; resuming ${kind} track`);
|
||||
p.track
|
||||
.resumeUpstream()
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
`Failed to resume ${kind} track after MatrixRTC reconnection`,
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const p of publications) {
|
||||
if (p.track?.isUpstreamPaused === false) {
|
||||
const kind = p.track.kind;
|
||||
logger.log(`Lost connection to MatrixRTC; pausing ${kind} track`);
|
||||
p.track
|
||||
.pauseUpstream()
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
`Failed to pause ${kind} track after MatrixRTC connection loss`,
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ export function mockLocalParticipant(
|
||||
): LocalParticipant {
|
||||
return {
|
||||
isLocal: true,
|
||||
trackPublications: new Map(),
|
||||
getTrackPublication: () =>
|
||||
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||
...mockEmitter(),
|
||||
|
||||
Reference in New Issue
Block a user