Compare commits
3
Commits
53823f5466
...
50b4e2c16c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b4e2c16c | ||
|
|
230d147ec1 | ||
|
|
9fefd14944 |
@@ -26,6 +26,7 @@ import {
|
||||
RoomEvent,
|
||||
} from 'matrix-js-sdk';
|
||||
import { IRTCNotificationContent, RTCNotificationType } from 'matrix-js-sdk/lib/matrixrtc/types';
|
||||
import { MatrixRTCSessionEvent } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
|
||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||
import {
|
||||
CallEmbedContextProvider,
|
||||
@@ -559,6 +560,50 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
setCallInfo(undefined);
|
||||
}, []);
|
||||
|
||||
// [Gitea #161] Stop ringing here when the call was handled ELSEWHERE:
|
||||
// - answered on another of our devices (our own m.call.member appears),
|
||||
// - declined on another of our devices (our own RTCDecline for this ring),
|
||||
// - the caller hung up before we picked up (nobody left in the session).
|
||||
// Without this every other device kept ringing for the full lifetime.
|
||||
useEffect(() => {
|
||||
if (!callInfo) return undefined;
|
||||
const { room, refEventId } = callInfo;
|
||||
const myUserId = mx.getSafeUserId();
|
||||
const dismiss = () =>
|
||||
setCallInfo((current) => (current?.refEventId === refEventId ? undefined : current));
|
||||
|
||||
const session = mx.matrixRTC.getRoomSession(room);
|
||||
const checkMemberships = () => {
|
||||
const { memberships } = session;
|
||||
if (memberships.some((m) => m.sender === myUserId))
|
||||
dismiss(); // answered elsewhere
|
||||
else if (memberships.length === 0) dismiss(); // caller gone
|
||||
};
|
||||
const onTimeline: EventTimelineSetHandlerMap[RoomEvent.Timeline] = (
|
||||
event,
|
||||
eventRoom,
|
||||
_s,
|
||||
_r,
|
||||
data,
|
||||
) => {
|
||||
if (eventRoom?.roomId !== room.roomId || !data.liveEvent) return;
|
||||
if (event.getType() === EventType.RTCDecline && event.getSender() === myUserId) {
|
||||
const related = event.getRelation()?.event_id;
|
||||
if (!related || related === refEventId) dismiss(); // declined elsewhere
|
||||
}
|
||||
};
|
||||
session.on(MatrixRTCSessionEvent.MembershipsChanged, checkMemberships);
|
||||
mx.on(RoomEvent.Timeline, onTimeline);
|
||||
// The caller's membership may not have arrived yet when the ring starts —
|
||||
// only treat "empty" as hung-up after it has had a moment to sync.
|
||||
const settle = setTimeout(checkMemberships, 5000);
|
||||
return () => {
|
||||
session.off(MatrixRTCSessionEvent.MembershipsChanged, checkMemberships);
|
||||
mx.removeListener(RoomEvent.Timeline, onTimeline);
|
||||
clearTimeout(settle);
|
||||
};
|
||||
}, [mx, callInfo]);
|
||||
|
||||
const handleReject = useCallback(
|
||||
(room: Room, eventId: string) => {
|
||||
// Best-effort: the local UI dismisses regardless (below), but a failed
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
|
||||
import { useAtom } from 'jotai';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||
import { logoutClient } from '../../client/initMatrix';
|
||||
import { callEmbedAtom } from '../state/callEmbed';
|
||||
import { CallEmbed } from '../plugins/call';
|
||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||
import { useModalStyle } from '../hooks/useModalStyle';
|
||||
import { useCrossSigningActive } from '../hooks/useCrossSigning';
|
||||
@@ -11,6 +15,32 @@ import {
|
||||
VerificationStatus,
|
||||
} from '../hooks/useDeviceVerificationStatus';
|
||||
|
||||
/**
|
||||
* Ask Element Call to hang up and wait (bounded) until our own MatrixRTC
|
||||
* membership has actually been removed from the room, so the leave reaches
|
||||
* 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 = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
@@ -26,10 +56,19 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
||||
mx.getDeviceId() ?? undefined,
|
||||
);
|
||||
|
||||
const [callEmbed, setCallEmbed] = useAtom(callEmbedAtom);
|
||||
|
||||
const [logoutState, logout] = useAsyncCallback<void, Error, []>(
|
||||
useCallback(async () => {
|
||||
// [Gitea #29] Logging out mid-call must hang up first, or the MatrixRTC
|
||||
// membership (expires: 4 h) stays behind as a ghost participant and
|
||||
// everyone else sees you "in call" until it times out.
|
||||
if (callEmbed && callEmbed.joined && !callEmbed.disposed) {
|
||||
await hangupAndWait(mx, callEmbed);
|
||||
setCallEmbed(undefined);
|
||||
}
|
||||
await logoutClient(mx);
|
||||
}, [mx]),
|
||||
}, [mx, callEmbed, setCallEmbed]),
|
||||
);
|
||||
|
||||
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
|
||||
|
||||
@@ -50,6 +50,11 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
// user-initiated unmute that auto-undeafens the user.
|
||||
public pttActive = false;
|
||||
|
||||
// Deafen mutes the mic; undeafen should give it back ONLY if it was on
|
||||
// before (Discord semantics — verified against the real client, Gitea #173:
|
||||
// undeafen used to leave you muted). Cleared by any manual mic toggle.
|
||||
private micOnBeforeDeafen = false;
|
||||
|
||||
// P6-2: mirrors CallEmbed.joined. Set true from forceState(), which CallEmbed
|
||||
// invokes only from onCallJoined(). Gates io.lotus.set_deafen so we never send
|
||||
// before the fork's widget handler mounts (pre-join sends pend to a 10s
|
||||
@@ -398,7 +403,11 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
|
||||
this.emitStateUpdate();
|
||||
|
||||
if (!this.sound && this.microphone) {
|
||||
if (!sound) {
|
||||
this.micOnBeforeDeafen = this.microphone;
|
||||
if (this.microphone) this.toggleMicrophone();
|
||||
} else if (this.micOnBeforeDeafen && !this.microphone) {
|
||||
this.micOnBeforeDeafen = false;
|
||||
this.toggleMicrophone();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user