mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
fix(notifications): request OS permission at startup and fix socketService listener leak (#998)
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
export { startNativeNotificationsService, stopNativeNotificationsService } from './service';
|
||||
export { showNativeNotification } from './tauriBridge';
|
||||
export { ensureNotificationPermission, showNativeNotification } from './tauriBridge';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type NotificationItem,
|
||||
notificationReceived,
|
||||
} from '../../store/notificationSlice';
|
||||
import { showNativeNotification } from './tauriBridge';
|
||||
import { ensureNotificationPermission, showNativeNotification } from './tauriBridge';
|
||||
|
||||
const log = debug('native-notifications');
|
||||
|
||||
@@ -52,18 +52,27 @@ function dispatchAndMaybeBanner(
|
||||
timestampOverride?: number
|
||||
): void {
|
||||
const prefs = store.getState().notifications.preferences;
|
||||
log(
|
||||
'[dispatch] category=%s id=%s enabled=%s focused=%s',
|
||||
category,
|
||||
item.id,
|
||||
prefs[category],
|
||||
windowIsFocused()
|
||||
);
|
||||
if (!prefs[category]) {
|
||||
log('category %s disabled, skipping', category);
|
||||
return;
|
||||
}
|
||||
const timestamp = timestampOverride && timestampOverride > 0 ? timestampOverride : Date.now();
|
||||
const full: NotificationItem = { ...item, category, timestamp, read: false };
|
||||
log('[dispatch] enqueue id=%s title=%s', full.id, full.title);
|
||||
store.dispatch(notificationReceived(full));
|
||||
// Only fire OS-level banner when the user isn't already looking at the
|
||||
// window — otherwise the in-app center is enough and a native toast is
|
||||
// redundant noise.
|
||||
if (!windowIsFocused()) {
|
||||
void showNativeNotification({ title: full.title, body: full.body, tag: full.id });
|
||||
log('[dispatch] window unfocused, firing native banner id=%s', full.id);
|
||||
void showNativeNotification({ title: full.title, body: full.body });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +91,15 @@ export function startNativeNotificationsService(): void {
|
||||
if (started) return;
|
||||
started = true;
|
||||
|
||||
// Request OS notification permission early so native banners can fire.
|
||||
// Fire-and-forget — permission state is logged for diagnostics.
|
||||
void ensureNotificationPermission().then(granted => {
|
||||
log('notification permission ensured: granted=%s', granted);
|
||||
});
|
||||
|
||||
chatDoneListener = (...args: unknown[]) => {
|
||||
const p = (args[0] ?? {}) as ChatDonePayload;
|
||||
log('[socket] chat_done');
|
||||
dispatchAndMaybeBanner('agents', {
|
||||
id: `chat_done:${p.thread_id ?? 'unknown'}:${p.request_id ?? Date.now()}`,
|
||||
title: 'Agent reply ready',
|
||||
@@ -94,6 +110,7 @@ export function startNativeNotificationsService(): void {
|
||||
|
||||
chatErrorListener = (...args: unknown[]) => {
|
||||
const p = (args[0] ?? {}) as ChatErrorPayload;
|
||||
log('[socket] chat_error');
|
||||
dispatchAndMaybeBanner('system', {
|
||||
id: `chat_error:${p.thread_id ?? 'unknown'}:${p.request_id ?? Date.now()}`,
|
||||
title: 'Agent error',
|
||||
@@ -107,7 +124,11 @@ export function startNativeNotificationsService(): void {
|
||||
// bus. See src/openhuman/notifications/bus.rs.
|
||||
coreNotificationListener = (...args: unknown[]) => {
|
||||
const p = (args[0] ?? {}) as CoreNotificationPayload;
|
||||
if (!p.id || !p.title) return;
|
||||
log('[socket] core_notification id=%s category=%s', p.id, p.category);
|
||||
if (!p.id || !p.title) {
|
||||
log('[socket] core_notification missing id/title dropped');
|
||||
return;
|
||||
}
|
||||
const serverTs = p.timestamp_ms && p.timestamp_ms > 0 ? p.timestamp_ms : Date.now();
|
||||
dispatchAndMaybeBanner(
|
||||
p.category,
|
||||
@@ -123,6 +144,7 @@ export function startNativeNotificationsService(): void {
|
||||
|
||||
disconnectListener = (...args: unknown[]) => {
|
||||
const reason = typeof args[0] === 'string' ? args[0] : 'unknown';
|
||||
log('[socket] disconnect reason=%s', reason);
|
||||
dispatchAndMaybeBanner('system', {
|
||||
id: `socket_disconnect:${Date.now()}`,
|
||||
title: 'Connection lost',
|
||||
|
||||
@@ -7,7 +7,38 @@ const errLog = debug('native-notifications:bridge:error');
|
||||
export interface ShowNativeNotificationArgs {
|
||||
title: string;
|
||||
body: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request OS notification permission if not already granted.
|
||||
* Returns true if permission is (or was just) granted, false otherwise.
|
||||
* No-op (returns false) when running outside Tauri.
|
||||
*/
|
||||
export async function ensureNotificationPermission(): Promise<boolean> {
|
||||
if (!isTauri()) {
|
||||
log('not running in tauri, skipping permission request');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const grantedRaw = await invoke<boolean | null>('plugin:notification|is_permission_granted');
|
||||
const granted = grantedRaw === true;
|
||||
log('notification permission check (plugin): granted=%s raw=%o', granted, grantedRaw);
|
||||
if (granted) return true;
|
||||
|
||||
const requestResult = await invoke<string>('plugin:notification|request_permission');
|
||||
const requestState = String(requestResult ?? 'unknown').toLowerCase();
|
||||
const nowGranted = requestState === 'granted' || requestState === 'provisional';
|
||||
log('notification permission request result=%s granted=%s', requestState, nowGranted);
|
||||
if (nowGranted) return true;
|
||||
|
||||
// Re-check once after request because some platforms may not return
|
||||
// a definitive granted state from request_permission directly.
|
||||
const pluginGranted = await invoke<boolean | null>('plugin:notification|is_permission_granted');
|
||||
return pluginGranted === true;
|
||||
} catch (err) {
|
||||
errLog('ensureNotificationPermission failed: %O', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,12 +51,10 @@ export async function showNativeNotification(args: ShowNativeNotificationArgs):
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await invoke('show_native_notification', {
|
||||
title: args.title,
|
||||
body: args.body,
|
||||
tag: args.tag ?? null,
|
||||
await invoke('plugin:notification|notify', {
|
||||
options: { title: args.title, body: args.body, sound: 'default' },
|
||||
});
|
||||
} catch (err) {
|
||||
errLog('show_native_notification failed: %O', err);
|
||||
errLog('plugin:notification|notify failed: %O', err);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user