mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 08:03:08 +00:00
123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
import * as Notifications from 'expo-notifications'
|
|
import { Platform } from 'react-native'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { loadPushNotificationsEnabled } from '../storage/preferences'
|
|
import { buildLocalNotificationData, type DesktopNotificationSource } from './notification-routing'
|
|
|
|
type NotificationEvent = {
|
|
type: 'notification'
|
|
source: DesktopNotificationSource
|
|
title: string
|
|
body: string
|
|
worktreeId?: string
|
|
}
|
|
|
|
type SubscribeResult = {
|
|
type: 'ready'
|
|
subscriptionId: string
|
|
}
|
|
|
|
export type NotificationPermissionState = {
|
|
granted: boolean
|
|
status: string
|
|
canAskAgain: boolean
|
|
}
|
|
|
|
export async function getNotificationPermissionState(): Promise<NotificationPermissionState> {
|
|
const { status, canAskAgain } = await Notifications.getPermissionsAsync()
|
|
return {
|
|
granted: status === 'granted',
|
|
status,
|
|
canAskAgain
|
|
}
|
|
}
|
|
|
|
// Why: permissions must be requested before scheduling any local notification.
|
|
// Read the OS state every time because users can change it in Settings while
|
|
// Orca remains alive in the background.
|
|
export async function ensureNotificationPermissions(): Promise<boolean> {
|
|
const existing = await getNotificationPermissionState()
|
|
if (existing.granted) {
|
|
return true
|
|
}
|
|
|
|
const { status } = await Notifications.requestPermissionsAsync()
|
|
return status === 'granted'
|
|
}
|
|
|
|
function configureNotificationChannel(): void {
|
|
if (Platform.OS === 'android') {
|
|
void Notifications.setNotificationChannelAsync('orca-desktop', {
|
|
name: 'Desktop Notifications',
|
|
importance: Notifications.AndroidImportance.HIGH,
|
|
vibrationPattern: [0, 250],
|
|
lightColor: '#6366f1'
|
|
})
|
|
}
|
|
}
|
|
|
|
async function showLocalNotification(event: NotificationEvent, hostId: string): Promise<void> {
|
|
const enabled = await loadPushNotificationsEnabled()
|
|
if (!enabled) return
|
|
|
|
const granted = await ensureNotificationPermissions()
|
|
if (!granted) return
|
|
|
|
await Notifications.scheduleNotificationAsync({
|
|
content: {
|
|
title: event.title,
|
|
body: event.body,
|
|
data: buildLocalNotificationData(event, hostId),
|
|
...(Platform.OS === 'android' ? { channelId: 'orca-desktop' } : {})
|
|
},
|
|
trigger: null
|
|
})
|
|
}
|
|
|
|
// Why: each host connection gets its own notification subscription. When the
|
|
// connection drops, the unsubscribe function cleans up the streaming RPC.
|
|
// Returns an unsubscribe function.
|
|
export function subscribeToDesktopNotifications(client: RpcClient, hostId: string): () => void {
|
|
configureNotificationChannel()
|
|
|
|
let subscriptionId: string | null = null
|
|
let disposed = false
|
|
function unsubscribeServer(id: string) {
|
|
if (client.getState() === 'connected') {
|
|
client.sendRequest('notifications.unsubscribe', { subscriptionId: id }).catch(() => {})
|
|
}
|
|
}
|
|
|
|
const unsubscribeStream = client.subscribe('notifications.subscribe', {}, (data: unknown) => {
|
|
const event = data as NotificationEvent | SubscribeResult | { type: 'end' }
|
|
if (event.type === 'ready') {
|
|
subscriptionId = (event as SubscribeResult).subscriptionId
|
|
if (disposed) {
|
|
unsubscribeServer(subscriptionId)
|
|
unsubscribeStream()
|
|
}
|
|
return
|
|
}
|
|
if (event.type === 'end') {
|
|
if (disposed) unsubscribeStream()
|
|
return
|
|
}
|
|
if (disposed) return
|
|
if (event.type === 'notification') {
|
|
void showLocalNotification(event as NotificationEvent, hostId)
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
disposed = true
|
|
// Why: the client may already be closed when this cleanup runs (component
|
|
// unmount races with disconnect). sendRequest rejects immediately on a
|
|
// closed client — swallow it since server-side cleanup happens via
|
|
// connection-close anyway.
|
|
if (subscriptionId) {
|
|
unsubscribeStream()
|
|
unsubscribeServer(subscriptionId)
|
|
}
|
|
}
|
|
}
|