Files
cinny/src/app/utils/callRejoin.ts
T
Lotus CIandClaude Opus 5.5 0a2f18e3d5
CI / Build & Quality Checks (pull_request) Successful in 3m16s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 19s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m35s
fix(calls): DM-aware rejoin toast, and no rejoining a DM call that ended (#302)
After a reload during a DM call the toast said "You were in the call in
Bob Builder"; it now says "You were in a call with Bob Builder". DM
calls are never auto-rejoined (it could ring the other person), and if
they have left by the time you tap, the toast says "Call ended" instead
of starting a new, ringing call.

Fixes #302

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 20:20:12 -04:00

103 lines
3.8 KiB
TypeScript

/**
* [Gitea #118] Remember the voice room you were in so a crash, update or
* reload can put you back. Device-local (localStorage), never synced;
* cleared on a deliberate hangup and on logout.
*/
export const CALL_SESSION_KEY = 'lotus-call-session';
/** A record older than this (by last heartbeat) is stale — the call is over. */
export const CALL_REJOIN_MAX_AGE_MS = 10 * 60_000;
export const CALL_SESSION_HEARTBEAT_MS = 30_000;
export type CallSessionRecord = {
roomId: string;
deviceId: string;
joinedAt: number;
/** Refreshed while joined so a crash leaves a recent timestamp behind. */
lastSeen: number;
microphone: boolean;
video: boolean;
};
export type CallRejoinMode = 'ask' | 'auto' | 'off';
export const readCallSession = (): CallSessionRecord | undefined => {
try {
const raw = localStorage.getItem(CALL_SESSION_KEY);
if (!raw) return undefined;
const r = JSON.parse(raw) as Partial<CallSessionRecord>;
if (typeof r.roomId !== 'string' || typeof r.deviceId !== 'string') return undefined;
return {
roomId: r.roomId,
deviceId: r.deviceId,
joinedAt: Number(r.joinedAt) || 0,
lastSeen: Number(r.lastSeen) || 0,
microphone: r.microphone !== false,
video: r.video === true,
};
} catch {
return undefined;
}
};
export const writeCallSession = (record: CallSessionRecord): void => {
try {
localStorage.setItem(CALL_SESSION_KEY, JSON.stringify(record));
} catch {
/* quota / private mode */
}
};
export const clearCallSession = (): void => {
try {
localStorage.removeItem(CALL_SESSION_KEY);
} catch {
/* ignore */
}
};
export type RejoinContext = {
record: CallSessionRecord | undefined;
mode: CallRejoinMode;
now: number;
myDeviceId: string;
/** Whether the room still exists and we are joined to it. */
roomJoined: boolean;
/** Device ids of our own live call memberships in that room (other tabs/devices). */
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 };
/** Pure: whether startup should offer (or perform) a rejoin. */
export function decideRejoin(ctx: RejoinContext): RejoinDecision {
const { record, mode, now, myDeviceId } = ctx;
if (!record || mode === 'off') return { action: 'none' };
if (record.deviceId !== myDeviceId) return { action: 'none' };
if (now - record.lastSeen > CALL_REJOIN_MAX_AGE_MS) return { action: 'none' };
if (!ctx.roomJoined) return { action: 'none' };
// Already back in from another device/tab — don't double-join.
if (ctx.ownMemberDevices.some((d) => d !== myDeviceId)) return { action: 'none' };
// 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 <room>". */
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.`;