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
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
103 lines
3.8 KiB
TypeScript
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.`;
|