Added app badge support for the guest PWA and wired it to the existing counts (unread notifications + upload queue + pending uploads). When the total hits zero, the badge is cleared; when it’s >0, it’s set.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
type GuestNotificationItem,
|
type GuestNotificationItem,
|
||||||
} from '../services/notificationApi';
|
} from '../services/notificationApi';
|
||||||
import { fetchPendingUploadsSummary } from '../services/pendingUploadsApi';
|
import { fetchPendingUploadsSummary } from '../services/pendingUploadsApi';
|
||||||
|
import { updateAppBadge } from '../lib/badges';
|
||||||
|
|
||||||
export type NotificationCenterValue = {
|
export type NotificationCenterValue = {
|
||||||
notifications: GuestNotificationItem[];
|
notifications: GuestNotificationItem[];
|
||||||
@@ -265,6 +266,10 @@ export function NotificationCenterProvider({ eventToken, children }: { eventToke
|
|||||||
const loading = loadingNotifications || queueLoading || pendingLoading;
|
const loading = loadingNotifications || queueLoading || pendingLoading;
|
||||||
const totalCount = unreadCount + queueCount + pendingCount;
|
const totalCount = unreadCount + queueCount + pendingCount;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
void updateAppBadge(totalCount);
|
||||||
|
}, [totalCount]);
|
||||||
|
|
||||||
const value: NotificationCenterValue = {
|
const value: NotificationCenterValue = {
|
||||||
notifications,
|
notifications,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
|
|||||||
52
resources/js/guest/lib/__tests__/badges.test.ts
Normal file
52
resources/js/guest/lib/__tests__/badges.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { supportsBadging, updateAppBadge } from '../badges';
|
||||||
|
|
||||||
|
const originalSet = (navigator as any).setAppBadge;
|
||||||
|
const originalClear = (navigator as any).clearAppBadge;
|
||||||
|
const hadSet = 'setAppBadge' in navigator;
|
||||||
|
const hadClear = 'clearAppBadge' in navigator;
|
||||||
|
|
||||||
|
function restoreNavigator() {
|
||||||
|
if (hadSet) {
|
||||||
|
Object.defineProperty(navigator, 'setAppBadge', { configurable: true, value: originalSet });
|
||||||
|
} else {
|
||||||
|
delete (navigator as any).setAppBadge;
|
||||||
|
}
|
||||||
|
if (hadClear) {
|
||||||
|
Object.defineProperty(navigator, 'clearAppBadge', { configurable: true, value: originalClear });
|
||||||
|
} else {
|
||||||
|
delete (navigator as any).clearAppBadge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('badges', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
restoreNavigator();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the badge count when supported', async () => {
|
||||||
|
const setAppBadge = vi.fn();
|
||||||
|
Object.defineProperty(navigator, 'setAppBadge', { configurable: true, value: setAppBadge });
|
||||||
|
Object.defineProperty(navigator, 'clearAppBadge', { configurable: true, value: vi.fn() });
|
||||||
|
|
||||||
|
expect(supportsBadging()).toBe(true);
|
||||||
|
await updateAppBadge(4);
|
||||||
|
expect(setAppBadge).toHaveBeenCalledWith(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the badge when count is zero', async () => {
|
||||||
|
const clearAppBadge = vi.fn();
|
||||||
|
Object.defineProperty(navigator, 'setAppBadge', { configurable: true, value: vi.fn() });
|
||||||
|
Object.defineProperty(navigator, 'clearAppBadge', { configurable: true, value: clearAppBadge });
|
||||||
|
|
||||||
|
await updateAppBadge(0);
|
||||||
|
expect(clearAppBadge).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no-ops when unsupported', async () => {
|
||||||
|
delete (navigator as any).setAppBadge;
|
||||||
|
delete (navigator as any).clearAppBadge;
|
||||||
|
expect(supportsBadging()).toBe(false);
|
||||||
|
await updateAppBadge(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
46
resources/js/guest/lib/badges.ts
Normal file
46
resources/js/guest/lib/badges.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
type BadgingNavigator = Navigator & {
|
||||||
|
setAppBadge?: (contents?: number) => Promise<void> | void;
|
||||||
|
clearAppBadge?: () => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNavigator(): BadgingNavigator | null {
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return navigator as BadgingNavigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function supportsBadging(): boolean {
|
||||||
|
const nav = getNavigator();
|
||||||
|
return Boolean(nav && (typeof nav.setAppBadge === 'function' || typeof nav.clearAppBadge === 'function'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAppBadge(count: number): Promise<void> {
|
||||||
|
const nav = getNavigator();
|
||||||
|
if (!nav) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeCount = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;
|
||||||
|
if (!supportsBadging()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (safeCount > 0 && nav.setAppBadge) {
|
||||||
|
await nav.setAppBadge(safeCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nav.clearAppBadge) {
|
||||||
|
await nav.clearAppBadge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nav.setAppBadge) {
|
||||||
|
await nav.setAppBadge(0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Updating app badge failed', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user