Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 3bf040f17a fix(notifications): in-app toasts no longer need browser notification permission (#324)
CI / Build & Quality Checks (pull_request) Successful in 3m2s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 11s
CI / Playwright smoke (e2e) (pull_request) Successful in 18m38s
notify() shows an in-page toast while the window is focused and only
falls back to an OS notification otherwise, but both message and invite
notifications were gated on Notification.permission === granted. Anyone
who declined (or was never asked) heard the ping with nothing on screen.
Gate only the OS path on the permission.

Fixes #324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 20:41:48 -04:00
4 changed files with 10 additions and 67 deletions
+1 -25
View File
@@ -17,8 +17,6 @@ import {
CALL_SESSION_HEARTBEAT_MS,
clearCallSession,
decideRejoin,
othersInCall,
rejoinToastBody,
readCallSession,
writeCallSession,
} from '../utils/callRejoin';
@@ -114,27 +112,6 @@ 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 {
@@ -156,7 +133,6 @@ 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.
@@ -176,7 +152,7 @@ export function useCallRejoin(embed: CallEmbed | undefined, joined: boolean): vo
id: TOAST_ID,
iconSrc: Icons.Phone,
displayName: 'Rejoin voice?',
body: rejoinToastBody(room.name, latest.current.directs.has(room.roomId)),
body: `You were in the call in ${room.name ?? 'a room'} before the restart. Tap to rejoin.`,
roomName: room.name ?? '',
roomId: room.roomId,
sticky: true,
+8 -2
View File
@@ -334,7 +334,9 @@ function InviteNotifications() {
if (quietActive) return;
if (showNotifications && notificationPermission('granted')) {
// [Gitea #324] The in-app toast (focused window) needs no browser
// permission; only the OS notification does.
if (showNotifications && (document.hasFocus() || notificationPermission('granted'))) {
notify(newCount);
}
if (notificationSound && inviteSoundId !== 'none') {
@@ -641,7 +643,11 @@ function MessageNotifications() {
if (quietActive) return;
if (showNotifications && notificationPermission('granted')) {
// [Gitea #324] notify() shows an in-app toast while the window is focused,
// which needs no browser permission; only its OS-notification path does.
// Gating both on the permission left people who declined it (most web
// users) with a ping sound and nothing on screen.
if (showNotifications && (document.hasFocus() || notificationPermission('granted'))) {
const avatarMxc =
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
notify({
+1 -25
View File
@@ -1,12 +1,6 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
CALL_REJOIN_MAX_AGE_MS,
decideRejoin,
othersInCall,
rejoinToastBody,
RejoinContext,
} from './callRejoin';
import { CALL_REJOIN_MAX_AGE_MS, decideRejoin, RejoinContext } from './callRejoin';
const base: RejoinContext = {
record: {
@@ -60,21 +54,3 @@ 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);
});
-15
View File
@@ -67,8 +67,6 @@ 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 };
@@ -85,18 +83,5 @@ 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 <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.`;