11import { useEffect , useRef , useCallback } from 'react'
22import { useQueryClient } from '@tanstack/react-query'
3+ import { useTheme } from 'next-themes'
34import { toast } from 'sonner'
45
56interface TaskUpdatedMessage {
@@ -16,6 +17,7 @@ interface NotificationMessage {
1617 notificationType : 'success' | 'info' | 'warning' | 'error'
1718 taskId ?: string
1819 playSound ?: boolean
20+ isCustomSound ?: boolean
1921 }
2022}
2123
@@ -31,6 +33,7 @@ const RECONNECT_INTERVAL = 2000
3133
3234export function useTaskSync ( ) {
3335 const queryClient = useQueryClient ( )
36+ const { resolvedTheme } = useTheme ( )
3437 const wsRef = useRef < WebSocket | null > ( null )
3538 const reconnectTimeoutRef = useRef < ReturnType < typeof setTimeout > | undefined > ( undefined )
3639 const reconnectAttemptsRef = useRef ( 0 )
@@ -43,23 +46,51 @@ export function useTaskSync() {
4346 if ( message . type === 'task:updated' ) {
4447 queryClient . invalidateQueries ( { queryKey : [ 'tasks' ] } )
4548 } else if ( message . type === 'notification' && 'payload' in message ) {
46- const { title, message : description , notificationType, playSound } = ( message as NotificationMessage ) . payload
49+ const { id, title, message : description , notificationType, playSound, isCustomSound } = ( message as NotificationMessage ) . payload
50+
51+ // Determine icon: goat if default sound enabled, otherwise theme-appropriate logo
52+ const useGoat = playSound && ! isCustomSound
53+ const iconUrl = useGoat
54+ ? '/goat.jpeg'
55+ : resolvedTheme === 'dark'
56+ ? '/logo-dark.jpg'
57+ : '/logo-light.jpg'
58+
59+ // Create icon element for toast
60+ const icon = (
61+ < img
62+ src = { iconUrl }
63+ alt = ""
64+ className = "size-4 rounded-sm object-cover"
65+ / >
66+ )
67+
68+ // Show toast with custom icon
4769 switch ( notificationType ) {
4870 case 'success' :
49- toast . success ( title , { description } )
71+ toast . success ( title , { description, icon } )
5072 break
5173 case 'error' :
52- toast . error ( title , { description } )
74+ toast . error ( title , { description, icon } )
5375 break
5476 case 'warning' :
55- toast . warning ( title , { description } )
77+ toast . warning ( title , { description, icon } )
5678 break
5779 case 'info' :
5880 default :
59- toast . info ( title , { description } )
81+ toast . info ( title , { description, icon } )
6082 break
6183 }
6284
85+ // Show browser notification (skip in iframe - desktop app handles natively)
86+ if ( 'Notification' in window && window . parent === window && Notification . permission === 'granted' ) {
87+ new Notification ( title , {
88+ body : description ,
89+ icon : iconUrl ,
90+ tag : id ,
91+ } )
92+ }
93+
6394 // Play notification sound if enabled
6495 // Try custom sound first (/api/uploads/sound), fall back to default
6596 // Use localStorage to prevent multiple tabs from playing the same sound
@@ -96,7 +127,7 @@ export function useTaskSync() {
96127 // Ignore parse errors
97128 }
98129 } ,
99- [ queryClient ]
130+ [ queryClient , resolvedTheme ]
100131 )
101132
102133 const connect = useCallback ( ( ) => {
@@ -135,6 +166,13 @@ export function useTaskSync() {
135166 // Keep connectRef in sync with connect
136167 connectRef . current = connect
137168
169+ // Request browser notification permission on first load
170+ useEffect ( ( ) => {
171+ if ( 'Notification' in window && Notification . permission === 'default' ) {
172+ Notification . requestPermission ( )
173+ }
174+ } , [ ] )
175+
138176 useEffect ( ( ) => {
139177 connect ( )
140178
0 commit comments