diff --git a/src/app/hooks/useCallRejoin.ts b/src/app/hooks/useCallRejoin.ts index f69bf0e99..e1d908ca9 100644 --- a/src/app/hooks/useCallRejoin.ts +++ b/src/app/hooks/useCallRejoin.ts @@ -17,6 +17,8 @@ import { CALL_SESSION_HEARTBEAT_MS, clearCallSession, decideRejoin, + othersInCall, + rejoinToastBody, readCallSession, writeCallSession, } from '../utils/callRejoin'; @@ -112,6 +114,27 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo }; dismissToast(TOAST_ID); clearCallSession(); + // [Gitea #302] A DM call the other person has since left is over; + // starting it again would ring them, so just say so. + const dm = l.directs.has(room.roomId); + if ( + dm && + !othersInCall( + session.memberships.map((m) => m.userId ?? ''), + mx.getUserId() ?? '', + ) + ) { + // Own id: clicking a toast removes it by id after its onClick runs. + setToast({ + id: `${TOAST_ID}-ended`, + iconSrc: Icons.Phone, + displayName: 'Call ended', + body: `${room.name ?? 'They'} already left the call.`, + roomName: room.name ?? '', + roomId: room.roomId, + }); + return; + } try { (l.directs.has(room.roomId) ? l.startDmCall : l.startRoomCall)(room, pref); } catch { @@ -133,6 +156,7 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo .filter((m) => m.userId === mx.getUserId()) .map((m) => m.deviceId), memberCount: memberships.length, + dm: latest.current.directs.has(room.roomId), }); // The RTC session fills in a moment after sync; keep waiting while it is // empty unless this is the final (timed-out) look. @@ -152,7 +176,7 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo id: TOAST_ID, iconSrc: Icons.Phone, displayName: 'Rejoin voice?', - body: `You were in the call in ${room.name ?? 'a room'} before the restart. Tap to rejoin.`, + body: rejoinToastBody(room.name, latest.current.directs.has(room.roomId)), roomName: room.name ?? '', roomId: room.roomId, sticky: true, diff --git a/src/app/utils/callRejoin.test.ts b/src/app/utils/callRejoin.test.ts index 9f5eb62fd..ad887d65d 100644 --- a/src/app/utils/callRejoin.test.ts +++ b/src/app/utils/callRejoin.test.ts @@ -1,6 +1,12 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { CALL_REJOIN_MAX_AGE_MS, decideRejoin, RejoinContext } from './callRejoin'; +import { + CALL_REJOIN_MAX_AGE_MS, + decideRejoin, + othersInCall, + rejoinToastBody, + RejoinContext, +} from './callRejoin'; const base: RejoinContext = { record: { @@ -54,3 +60,21 @@ describe('decideRejoin', () => { }); }); }); + +it('decideRejoin: DM calls are never auto-rejoined, only offered (#302)', () => { + assert.deepEqual(decideRejoin({ ...base, mode: 'auto', dm: true }), { + action: 'ask', + roomId: '!r', + }); + assert.deepEqual(decideRejoin({ ...base, mode: 'auto' }), { action: 'auto', roomId: '!r' }); +}); + +it('rejoinToastBody and othersInCall (#302)', () => { + assert.equal( + rejoinToastBody('Bob Builder', true), + 'You were in a call with Bob Builder before the restart. Tap to rejoin.', + ); + assert.match(rejoinToastBody('Voice Lounge', false), /in the call in Voice Lounge/); + assert.equal(othersInCall(['@me:x'], '@me:x'), false); + assert.equal(othersInCall(['@me:x', '@bob:x'], '@me:x'), true); +}); diff --git a/src/app/utils/callRejoin.ts b/src/app/utils/callRejoin.ts index 93cfb365b..fc53cd6fc 100644 --- a/src/app/utils/callRejoin.ts +++ b/src/app/utils/callRejoin.ts @@ -67,6 +67,8 @@ export type RejoinContext = { ownMemberDevices: string[]; /** Number of call memberships in the room, ours included. */ memberCount: number; + /** A DM call: rejoining one may ring the other person. */ + dm?: boolean; }; export type RejoinDecision = { action: 'none' } | { action: 'ask' | 'auto'; roomId: string }; @@ -83,5 +85,18 @@ export function decideRejoin(ctx: RejoinContext): RejoinDecision { // Nobody there any more (our own stale membership doesn't count). const others = ctx.memberCount - ctx.ownMemberDevices.length; if (others <= 0) return { action: 'none' }; + // [Gitea #302] Never rejoin a DM call on our own: if the other person has + // just left, starting it again would ring them. Ask instead. + if (ctx.dm && mode === 'auto') return { action: 'ask', roomId: record.roomId }; return { action: mode, roomId: record.roomId }; } + +/** Pure: whether anyone but us is still in the call (by call membership). */ +export const othersInCall = (memberUserIds: string[], myUserId: string): boolean => + memberUserIds.some((u) => u !== myUserId); + +/** The rejoin toast's text; DMs name the person rather than "the call in ". */ +export const rejoinToastBody = (roomName: string | undefined, dm: boolean): string => + dm + ? `You were in a call with ${roomName ?? 'them'} before the restart. Tap to rejoin.` + : `You were in the call in ${roomName ?? 'a room'} before the restart. Tap to rejoin.`;