Compare commits
5
Commits
68ce13f081
...
a475531b2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a475531b2b | ||
|
|
34574178a9 | ||
|
|
070a1ea012 | ||
|
|
bbcbdad55a | ||
|
|
a9e0b893be |
@@ -34,18 +34,19 @@ import {
|
|||||||
useCallHangupEvent,
|
useCallHangupEvent,
|
||||||
useCallJoined,
|
useCallJoined,
|
||||||
useCallThemeSync,
|
useCallThemeSync,
|
||||||
useCallMemberSoundSync,
|
|
||||||
useCallStart,
|
useCallStart,
|
||||||
} from '../hooks/useCallEmbed';
|
} from '../hooks/useCallEmbed';
|
||||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
||||||
import { toastQueueAtom } from '../state/toast';
|
import { toastQueueAtom } from '../state/toast';
|
||||||
import { CallEmbed, useCallControlState } from '../plugins/call';
|
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||||
|
import { hangupCallAndWait } from '../plugins/call/hangup';
|
||||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
import { previewRingtone, startRingtone, unlockRingtoneAudio } from '../utils/ringtones';
|
import { previewRingtone, startRingtone, unlockRingtoneAudio } from '../utils/ringtones';
|
||||||
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
|
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
|
||||||
import { useCallJoinLeaveSounds } from '../hooks/useCallJoinLeaveSounds';
|
import { useCallJoinLeaveSounds } from '../hooks/useCallJoinLeaveSounds';
|
||||||
|
import { useCallPolicyRevokedToast } from '../hooks/useCallPolicyRevokedToast';
|
||||||
import { useCallHotkeys } from '../hooks/useCallHotkeys';
|
import { useCallHotkeys } from '../hooks/useCallHotkeys';
|
||||||
import { useAfkAutoMute } from '../hooks/useAfkAutoMute';
|
import { useAfkAutoMute } from '../hooks/useAfkAutoMute';
|
||||||
import { useCallQuality } from '../hooks/useCallQuality';
|
import { useCallQuality } from '../hooks/useCallQuality';
|
||||||
@@ -422,6 +423,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
|
const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
|
||||||
const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
|
const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
|
||||||
const startCall = useCallStart(dm);
|
const startCall = useCallStart(dm);
|
||||||
|
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||||
const { microphone, sound } = useCallPreferences();
|
const { microphone, sound } = useCallPreferences();
|
||||||
const [cameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
const [cameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
||||||
|
|
||||||
@@ -623,16 +625,26 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleAnswer = useCallback(
|
const handleAnswer = useCallback(
|
||||||
(room: Room, video: boolean) => {
|
async (room: Room, video: boolean) => {
|
||||||
|
setCallInfo(undefined);
|
||||||
|
// [Gitea #195] Answering from the in-call banner is the one call-to-call
|
||||||
|
// switch path. Disposing the current embed alone leaves our ghost
|
||||||
|
// `m.call.member` in the old room for ~17 s (until the delayed leave
|
||||||
|
// expires) — hang up explicitly and wait for the membership to clear.
|
||||||
|
if (callEmbed?.joined) {
|
||||||
|
await hangupCallAndWait(mx, callEmbed);
|
||||||
|
// Dispose it ourselves now: its HangupCall echo would otherwise land
|
||||||
|
// after startCall() and clear the NEW embed from the atom.
|
||||||
|
setCallEmbed(undefined);
|
||||||
|
}
|
||||||
// Honour cameraOnJoin and the persisted mic/sound preferences instead of
|
// Honour cameraOnJoin and the persisted mic/sound preferences instead of
|
||||||
// forcing camera+mic+sound on — every other join path does this, and
|
// forcing camera+mic+sound on — every other join path does this, and
|
||||||
// Answer was skipping it, publishing the camera with no prescreen.
|
// Answer was skipping it, publishing the camera with no prescreen.
|
||||||
// (PTT's forceAudioOff is applied downstream inside useCallStart.)
|
// (PTT's forceAudioOff is applied downstream inside useCallStart.)
|
||||||
startCall(room, { microphone, video: cameraOnJoin && video, sound });
|
startCall(room, { microphone, video: cameraOnJoin && video, sound });
|
||||||
setCallInfo(undefined);
|
|
||||||
navigateRoom(room.roomId);
|
navigateRoom(room.roomId);
|
||||||
},
|
},
|
||||||
[startCall, navigateRoom, microphone, sound, cameraOnJoin],
|
[startCall, navigateRoom, microphone, sound, cameraOnJoin, callEmbed, mx, setCallEmbed],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!callInfo) return null;
|
if (!callInfo) return null;
|
||||||
@@ -673,8 +685,8 @@ function CallUtils({ embed, joined }: { embed: CallEmbed; joined: boolean }) {
|
|||||||
// the mobile in-call chat. Both are gated on `joined`.
|
// the mobile in-call chat. Both are gated on `joined`.
|
||||||
useCallHotkeys(embed, joined);
|
useCallHotkeys(embed, joined);
|
||||||
useAfkAutoMute(joined ? embed : undefined);
|
useAfkAutoMute(joined ? embed : undefined);
|
||||||
useCallMemberSoundSync(embed);
|
|
||||||
useCallJoinLeaveSounds(embed);
|
useCallJoinLeaveSounds(embed);
|
||||||
|
useCallPolicyRevokedToast(embed, joined);
|
||||||
useCallThemeSync(embed);
|
useCallThemeSync(embed);
|
||||||
useCallQuality(embed);
|
useCallQuality(embed);
|
||||||
useCallHangupEvent(
|
useCallHangupEvent(
|
||||||
@@ -1231,7 +1243,15 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
|||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '50%',
|
height: '50%',
|
||||||
...(callVisible && !pipMode ? wallpaperStyle : {}),
|
// [Gitea #224] The fork renders EC transparent (lotusTransparent) so the
|
||||||
|
// wallpaper shows through the full-size embed; in PiP the thing behind
|
||||||
|
// the frame is whatever room you navigated to, so give it an opaque
|
||||||
|
// surface instead of letting the timeline bleed through the tiles.
|
||||||
|
...(pipMode
|
||||||
|
? { background: color.Surface.Container }
|
||||||
|
: callVisible
|
||||||
|
? wallpaperStyle
|
||||||
|
: {}),
|
||||||
}}
|
}}
|
||||||
ref={callEmbedRef}
|
ref={callEmbedRef}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, { forwardRef, useCallback } from 'react';
|
import React, { forwardRef, useCallback } from 'react';
|
||||||
import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
|
import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
|
||||||
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
import { logoutClient } from '../../client/initMatrix';
|
import { logoutClient } from '../../client/initMatrix';
|
||||||
import { callEmbedAtom } from '../state/callEmbed';
|
import { callEmbedAtom } from '../state/callEmbed';
|
||||||
import { CallEmbed } from '../plugins/call';
|
import { hangupCallAndWait } from '../plugins/call/hangup';
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
import { useModalStyle } from '../hooks/useModalStyle';
|
import { useModalStyle } from '../hooks/useModalStyle';
|
||||||
import { useCrossSigningActive } from '../hooks/useCrossSigning';
|
import { useCrossSigningActive } from '../hooks/useCrossSigning';
|
||||||
@@ -20,27 +19,6 @@ import {
|
|||||||
* membership has actually been removed from the room, so the leave reaches
|
* membership has actually been removed from the room, so the leave reaches
|
||||||
* the homeserver before the client is stopped and the token is revoked.
|
* the homeserver before the client is stopped and the token is revoked.
|
||||||
*/
|
*/
|
||||||
const hangupAndWait = async (mx: MatrixClient, embed: CallEmbed): Promise<void> => {
|
|
||||||
const myUserId = mx.getUserId();
|
|
||||||
const myDeviceId = mx.getDeviceId();
|
|
||||||
const stillIn = (): boolean =>
|
|
||||||
mx.matrixRTC
|
|
||||||
.getRoomSession(embed.room)
|
|
||||||
.memberships.some((m) => m.sender === myUserId && m.deviceId === myDeviceId);
|
|
||||||
try {
|
|
||||||
await embed.hangup();
|
|
||||||
} catch {
|
|
||||||
// widget already gone — fall through to the wait/timeout
|
|
||||||
}
|
|
||||||
const deadline = Date.now() + 4000;
|
|
||||||
while (stillIn() && Date.now() < deadline) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await new Promise((r) => {
|
|
||||||
setTimeout(r, 150);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type LogoutDialogProps = {
|
type LogoutDialogProps = {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
@@ -64,7 +42,7 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
|||||||
// membership (expires: 4 h) stays behind as a ghost participant and
|
// membership (expires: 4 h) stays behind as a ghost participant and
|
||||||
// everyone else sees you "in call" until it times out.
|
// everyone else sees you "in call" until it times out.
|
||||||
if (callEmbed && callEmbed.joined && !callEmbed.disposed) {
|
if (callEmbed && callEmbed.joined && !callEmbed.disposed) {
|
||||||
await hangupAndWait(mx, callEmbed);
|
await hangupCallAndWait(mx, callEmbed);
|
||||||
setCallEmbed(undefined);
|
setCallEmbed(undefined);
|
||||||
}
|
}
|
||||||
await logoutClient(mx);
|
await logoutClient(mx);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import {
|
|||||||
RectCords,
|
RectCords,
|
||||||
Badge,
|
Badge,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
|
TooltipProvider,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useFocusWithin, useHover } from 'react-aria';
|
import { useFocusWithin, useHover } from 'react-aria';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
@@ -41,7 +43,12 @@ import isYesterday from 'dayjs/plugin/isYesterday';
|
|||||||
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav';
|
||||||
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
|
||||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl, getStateEvent } from '../../utils/room';
|
import {
|
||||||
|
getDirectRoomAvatarUrl,
|
||||||
|
getMemberName,
|
||||||
|
getRoomAvatarUrl,
|
||||||
|
getStateEvent,
|
||||||
|
} from '../../utils/room';
|
||||||
import { nameInitials } from '../../utils/common';
|
import { nameInitials } from '../../utils/common';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRoomUnread } from '../../state/hooks/unread';
|
import { useRoomUnread } from '../../state/hooks/unread';
|
||||||
@@ -690,6 +697,14 @@ function RoomNavItem_({
|
|||||||
const optionsVisible = hover || !!menuAnchor;
|
const optionsVisible = hover || !!menuAnchor;
|
||||||
const callSession = useCallSession(room);
|
const callSession = useCallSession(room);
|
||||||
const callMembers = useCallMembers(callSession);
|
const callMembers = useCallMembers(callSession);
|
||||||
|
const liveNames = useMemo(() => {
|
||||||
|
const names = Array.from(
|
||||||
|
new Set(callMembers.map((m) => m.sender).filter((u): u is string => !!u)),
|
||||||
|
).map((u) => getMemberName(room, u));
|
||||||
|
const shown = names.slice(0, 6);
|
||||||
|
const more = names.length - shown.length;
|
||||||
|
return more > 0 ? `${shown.join(', ')} and ${more} more` : shown.join(', ');
|
||||||
|
}, [callMembers, room]);
|
||||||
const startCall = useCallStart(direct);
|
const startCall = useCallStart(direct);
|
||||||
const callEmbed = useCallEmbed();
|
const callEmbed = useCallEmbed();
|
||||||
// [Gitea #30] Same voice-limit check the call prescreen uses, so the sidebar
|
// [Gitea #30] Same voice-limit check the call prescreen uses, so the sidebar
|
||||||
@@ -853,11 +868,32 @@ function RoomNavItem_({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{callMembers.length > 0 && (
|
{callMembers.length > 0 && (
|
||||||
<Badge variant="Critical" fill="Solid" size="400">
|
// [Gitea #157] The count alone doesn't tell you whether it's your
|
||||||
<Text as="span" size="L400" truncate>
|
// squad — name the people on hover/focus (long-press on touch).
|
||||||
{callMembers.length} Live
|
<TooltipProvider
|
||||||
</Text>
|
position="Right"
|
||||||
</Badge>
|
delay={300}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{liveNames}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(tipRef) => (
|
||||||
|
<Badge
|
||||||
|
ref={tipRef}
|
||||||
|
variant="Critical"
|
||||||
|
fill="Solid"
|
||||||
|
size="400"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`${callMembers.length} in call: ${liveNames}`}
|
||||||
|
>
|
||||||
|
<Text as="span" size="L400" truncate>
|
||||||
|
{callMembers.length} Live
|
||||||
|
</Text>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</NavItemContent>
|
</NavItemContent>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { ThemeKind, useTheme } from './useTheme';
|
|||||||
import { callEmbedAtom } from '../state/callEmbed';
|
import { callEmbedAtom } from '../state/callEmbed';
|
||||||
import { useResizeObserver } from './useResizeObserver';
|
import { useResizeObserver } from './useResizeObserver';
|
||||||
import { CallControlState } from '../plugins/call/CallControlState';
|
import { CallControlState } from '../plugins/call/CallControlState';
|
||||||
import { useCallMembersChange, useCallSession } from './useCall';
|
|
||||||
import { CallPreferences } from '../state/callPreferences';
|
import { CallPreferences } from '../state/callPreferences';
|
||||||
import { useSetting } from '../state/hooks/settings';
|
import { useSetting } from '../state/hooks/settings';
|
||||||
import { NoiseSuppressionMode, settingsAtom } from '../state/settings';
|
import { NoiseSuppressionMode, settingsAtom } from '../state/settings';
|
||||||
@@ -184,14 +183,6 @@ export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => {
|
|||||||
useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
|
useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCallMemberSoundSync = (embed: CallEmbed) => {
|
|
||||||
const callSession = useCallSession(embed.room);
|
|
||||||
useCallMembersChange(
|
|
||||||
callSession,
|
|
||||||
useCallback(() => embed.control.applySound(), [embed]),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCallThemeSync = (embed: CallEmbed) => {
|
export const useCallThemeSync = (embed: CallEmbed) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||||
|
import { useRoomCallPolicy } from './useRoomCallPolicy';
|
||||||
|
import { toastQueueAtom } from '../state/toast';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Gitea #223] When a room admin turns Allow Screen Sharing / Allow Camera
|
||||||
|
* off mid-call, the voice-limit-guard revokes the publish permission and the
|
||||||
|
* SFU drops our track within seconds — the button just vanishes with no
|
||||||
|
* explanation. Tell the user why: a track that went away while the room policy
|
||||||
|
* forbids it was not their doing.
|
||||||
|
*/
|
||||||
|
export function useCallPolicyRevokedToast(embed: CallEmbed, joined: boolean): void {
|
||||||
|
const { screenshare, video } = useCallControlState(embed.control);
|
||||||
|
const { allowScreenshare, allowCamera } = useRoomCallPolicy(embed.room);
|
||||||
|
const setToast = useSetAtom(toastQueueAtom);
|
||||||
|
const prev = useRef({ screenshare, video });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const was = prev.current;
|
||||||
|
prev.current = { screenshare, video };
|
||||||
|
if (!joined) return;
|
||||||
|
const notify = (body: string) =>
|
||||||
|
setToast({
|
||||||
|
id: `call-policy-${Date.now()}`,
|
||||||
|
displayName: 'Lotus Chat',
|
||||||
|
body,
|
||||||
|
roomName: embed.room.name ?? 'Voice call',
|
||||||
|
roomId: embed.roomId,
|
||||||
|
});
|
||||||
|
if (was.screenshare && !screenshare && !allowScreenshare) {
|
||||||
|
notify('Screen sharing was turned off by a room admin.');
|
||||||
|
}
|
||||||
|
if (was.video && !video && !allowCamera) {
|
||||||
|
notify('Your camera was turned off by a room admin.');
|
||||||
|
}
|
||||||
|
}, [screenshare, video, allowScreenshare, allowCamera, joined, setToast, embed]);
|
||||||
|
}
|
||||||
@@ -159,7 +159,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
audio_enabled: this.microphone,
|
audio_enabled: this.microphone,
|
||||||
video_enabled: this.video,
|
video_enabled: this.video,
|
||||||
});
|
});
|
||||||
this.setSound(this.sound);
|
this.setSound();
|
||||||
this.emitStateUpdate();
|
this.emitStateUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,10 +251,6 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.onControlMutation();
|
this.onControlMutation();
|
||||||
}
|
}
|
||||||
|
|
||||||
public applySound() {
|
|
||||||
this.setSound(this.sound);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setMediaState(state: ElementMediaStatePayload) {
|
private async setMediaState(state: ElementMediaStatePayload) {
|
||||||
// transport.send resolves once EC has ACK'd the command, which is enough to
|
// transport.send resolves once EC has ACK'd the command, which is enough to
|
||||||
// consider the mute applied. We deliberately do NOT gate completion on a
|
// consider the mute applied. We deliberately do NOT gate completion on a
|
||||||
@@ -265,33 +261,22 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
return this.call.transport.send(ElementWidgetActions.DeviceMute, state);
|
return this.call.transport.send(ElementWidgetActions.DeviceMute, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSound(sound: boolean): void {
|
// [Gitea #209] Deafen is applied by the fork (io.lotus.set_deafen). The
|
||||||
const callDocument = getCallDocument(this.iframe);
|
// iframe-DOM `.muted` fallback that used to live here (plus the per-member
|
||||||
if (callDocument) {
|
// re-apply in useCallMemberSoundSync) fought EC's audio renderer and is gone
|
||||||
callDocument.querySelectorAll('audio').forEach((el) => {
|
// now that the pin is ≥ 0.25.0-lotus.2 — late joiners are the fork's job.
|
||||||
const isScreenshareAudio = el.getAttribute('data-lk-source') === 'screen_share_audio';
|
private setSound(): void {
|
||||||
el.muted = !sound || (isScreenshareAudio && this.screenshareAudioMuted);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.sendDeafenState();
|
this.sendDeafenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyScreenshareAudioMuted(): void {
|
private applyScreenshareAudioMuted(): void {
|
||||||
if (!this.sound) return;
|
if (!this.sound) return;
|
||||||
const callDocument = getCallDocument(this.iframe);
|
|
||||||
if (callDocument) {
|
|
||||||
callDocument
|
|
||||||
.querySelectorAll<HTMLAudioElement>('audio[data-lk-source="screen_share_audio"]')
|
|
||||||
.forEach((el) => {
|
|
||||||
el.muted = this.screenshareAudioMuted;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.sendDeafenState();
|
this.sendDeafenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// P6-2: send deafen state to the fork (io.lotus.set_deafen). The DOM .muted
|
// P6-2: send deafen state to the fork (io.lotus.set_deafen). Join-gated: the
|
||||||
// code above is a transitional fallback — remove once the fork ships & the
|
// fork's handler only exists once joined; onCallJoined() re-sends the current
|
||||||
// pin is bumped.
|
// state so a pre-join deafen is not lost.
|
||||||
private sendDeafenState(): void {
|
private sendDeafenState(): void {
|
||||||
if (!this.joined) return;
|
if (!this.joined) return;
|
||||||
this.call.transport
|
this.call.transport
|
||||||
@@ -398,7 +383,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
);
|
);
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
this.setSound(sound);
|
this.setSound();
|
||||||
// After un-deafening, re-apply screenshare audio mute if active
|
// After un-deafening, re-apply screenshare audio mute if active
|
||||||
if (sound) this.applyScreenshareAudioMuted();
|
if (sound) this.applyScreenshareAudioMuted();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import { CallEmbed } from './CallEmbed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask EC to hang up and wait (bounded) until our own RTC membership has left
|
||||||
|
* the room's session. Used wherever the embed is about to be torn down for a
|
||||||
|
* reason other than the user pressing End — logout (#29) and answering another
|
||||||
|
* call while in one (#195) — because disposing the iframe alone leaves a ghost
|
||||||
|
* `m.call.member` for everyone else until the delayed leave event expires
|
||||||
|
* (~17–30 s measured).
|
||||||
|
*/
|
||||||
|
export const hangupCallAndWait = async (
|
||||||
|
mx: MatrixClient,
|
||||||
|
embed: CallEmbed,
|
||||||
|
timeoutMs = 4000,
|
||||||
|
): Promise<void> => {
|
||||||
|
const myUserId = mx.getUserId();
|
||||||
|
const myDeviceId = mx.getDeviceId();
|
||||||
|
const stillIn = (): boolean =>
|
||||||
|
mx.matrixRTC
|
||||||
|
.getRoomSession(embed.room)
|
||||||
|
.memberships.some((m) => m.sender === myUserId && m.deviceId === myDeviceId);
|
||||||
|
try {
|
||||||
|
await embed.hangup();
|
||||||
|
} catch {
|
||||||
|
// widget already gone — fall through to the wait/timeout
|
||||||
|
}
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (stillIn() && Date.now() < deadline) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise((r) => {
|
||||||
|
setTimeout(r, 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user