Skip to content

Commit e85a91c

Browse files
Merge branch 'improve-notifications-bfv1'
2 parents 89a0376 + 57098ff commit e85a91c

3 files changed

Lines changed: 48 additions & 8 deletions

File tree

public/goat.jpeg

362 KB
Loading

server/services/notification-service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ async function sendPushoverNotification(
147147
}
148148

149149
// Broadcast notification to UI via WebSocket
150-
function broadcastUINotification(payload: NotificationPayload, playSound: boolean): void {
150+
function broadcastUINotification(payload: NotificationPayload, playSound: boolean, isCustomSound: boolean): void {
151151
const notificationType =
152152
payload.type === 'pr_merged' || payload.type === 'plan_complete' ? 'success' : 'info'
153153

@@ -160,6 +160,7 @@ function broadcastUINotification(payload: NotificationPayload, playSound: boolea
160160
notificationType,
161161
taskId: payload.taskId,
162162
playSound, // Tell desktop app to play local sound
163+
isCustomSound, // Whether user has custom sound file (affects notification icon)
163164
},
164165
})
165166
}
@@ -178,9 +179,10 @@ export async function sendNotification(payload: NotificationPayload): Promise<No
178179
// Determine if sound should be played
179180
// Pass this to UI so desktop app can play sound locally
180181
const playSound = settings.sound?.enabled ?? false
182+
const isCustomSound = !!settings.sound?.customSoundFile
181183

182184
// Always broadcast to UI (with sound flag for desktop app)
183-
broadcastUINotification(payload, playSound)
185+
broadcastUINotification(payload, playSound, isCustomSound)
184186

185187
// Sound (macOS only)
186188
if (settings.sound?.enabled) {

src/hooks/use-task-sync.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useRef, useCallback } from 'react'
22
import { useQueryClient } from '@tanstack/react-query'
3+
import { useTheme } from 'next-themes'
34
import { toast } from 'sonner'
45

56
interface 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

3234
export 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

Comments
 (0)