fix: enable web runtime notifications without push (#1322)
Web notifications now listen to the notification stream directly Native notifications no longer depend on PWA push being enabled Window focus is respected when deciding whether to send background push notifications
This commit is contained in:
@@ -12,6 +12,7 @@ import { useMenuActions } from '@/hooks/useMenuActions';
|
||||
import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap';
|
||||
import { useRouter } from '@/hooks/useRouter';
|
||||
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
||||
import { useWebNotificationStream } from '@/hooks/useWebNotificationStream';
|
||||
import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt';
|
||||
import { useWindowTitle } from '@/hooks/useWindowTitle';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
@@ -621,6 +622,7 @@ function App({ apis }: AppProps) {
|
||||
// Session attention now handled by notification-store via SSE events (session.idle/session.error)
|
||||
|
||||
usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled });
|
||||
useWebNotificationStream({ enabled: embeddedBackgroundWorkEnabled });
|
||||
usePwaInstallPrompt();
|
||||
|
||||
useWindowTitle();
|
||||
|
||||
@@ -6,8 +6,7 @@ const HEARTBEAT_MS = 20000;
|
||||
|
||||
const resolveVisibilityState = (): 'visible' | 'hidden' => {
|
||||
if (typeof document === 'undefined') return 'visible';
|
||||
const state = document.visibilityState;
|
||||
return state === 'hidden' && document.hasFocus() ? 'visible' : state;
|
||||
return document.visibilityState === 'visible' && document.hasFocus() ? 'visible' : 'hidden';
|
||||
};
|
||||
|
||||
const sendVisibility = (visible: boolean) => {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import type { NotificationPayload } from '@/lib/api/types';
|
||||
|
||||
const NOTIFICATION_STREAM_PATH = '/api/notifications/stream';
|
||||
|
||||
const isFocused = () => {
|
||||
if (typeof document === 'undefined') return true;
|
||||
return document.visibilityState === 'visible' && document.hasFocus();
|
||||
};
|
||||
|
||||
const toNotificationPayload = (value: unknown): NotificationPayload | null => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
const properties = record.properties && typeof record.properties === 'object'
|
||||
? record.properties as Record<string, unknown>
|
||||
: null;
|
||||
if (record.type !== 'openchamber:notification' || !properties) return null;
|
||||
return {
|
||||
title: typeof properties.title === 'string' ? properties.title : undefined,
|
||||
body: typeof properties.body === 'string' ? properties.body : undefined,
|
||||
tag: typeof properties.tag === 'string' ? properties.tag : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const useWebNotificationStream = (options?: { enabled?: boolean }) => {
|
||||
const enabled = options?.enabled ?? true;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled || !isWebRuntime() || typeof window === 'undefined' || typeof EventSource === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = new EventSource(NOTIFICATION_STREAM_PATH);
|
||||
source.onmessage = (event) => {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(event.data) as unknown;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = useUIStore.getState();
|
||||
if (!settings.nativeNotificationsEnabled) return;
|
||||
if (settings.notificationMode !== 'always' && isFocused()) return;
|
||||
|
||||
const payload = toNotificationPayload(data);
|
||||
if (!payload) return;
|
||||
|
||||
const apis = getRegisteredRuntimeAPIs();
|
||||
void apis?.notifications?.notifyAgentCompletion(payload);
|
||||
};
|
||||
|
||||
return () => {
|
||||
source.close();
|
||||
};
|
||||
}, [enabled]);
|
||||
};
|
||||
@@ -162,6 +162,8 @@ export const registerNotificationRoutes = (app, dependencies) => {
|
||||
});
|
||||
|
||||
app.get('/api/notifications/stream', async (req, res) => {
|
||||
await ensureSessionWatcher();
|
||||
|
||||
const uiToken = uiAuthController?.ensureSessionToken
|
||||
? await uiAuthController.ensureSessionToken(req, res)
|
||||
: getUiSessionTokenFromRequest(req);
|
||||
|
||||
Reference in New Issue
Block a user