diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9bec14b..038d87d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import { useConversationActions, useConversationNavigation, useRealtimeAppState, + useBrowserNotifications, } from './hooks'; import { AppShell } from './components/AppShell'; import type { MessageInputHandle } from './components/MessageInput'; @@ -22,6 +23,13 @@ import type { Conversation, RawPacket } from './types'; export function App() { const messageInputRef = useRef(null); const [rawPackets, setRawPackets] = useState([]); + const { + notificationsSupported, + notificationsPermission, + isConversationNotificationsEnabled, + toggleConversationNotifications, + notifyIncomingMessage, + } = useBrowserNotifications(); const { showNewMessage, showSettings, @@ -202,6 +210,7 @@ export function App() { pendingDeleteFallbackRef, setActiveConversation, updateMessageAck, + notifyIncomingMessage, }); const { handleSendMessage, @@ -237,7 +246,10 @@ export function App() { [fetchUndecryptedCount, setChannels] ); - const statusProps = { health, config }; + const statusProps = { + health, + config, + }; const sidebarProps = { contacts, channels, @@ -289,6 +301,21 @@ export function App() { onLoadNewer: fetchNewerMessages, onJumpToBottom: jumpToBottom, onSendMessage: handleSendMessage, + notificationsSupported, + notificationsPermission, + notificationsEnabled: + activeConversation?.type === 'contact' || activeConversation?.type === 'channel' + ? isConversationNotificationsEnabled(activeConversation.type, activeConversation.id) + : false, + onToggleNotifications: () => { + if (activeConversation?.type === 'contact' || activeConversation?.type === 'channel') { + void toggleConversationNotifications( + activeConversation.type, + activeConversation.id, + activeConversation.name + ); + } + }, }; const searchProps = { contacts, diff --git a/frontend/src/components/ChatHeader.tsx b/frontend/src/components/ChatHeader.tsx index 1537bec..2dbbaaf 100644 --- a/frontend/src/components/ChatHeader.tsx +++ b/frontend/src/components/ChatHeader.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Globe2, Info, Route, Star, Trash2 } from 'lucide-react'; +import { Bell, Globe2, Info, Route, Star, Trash2 } from 'lucide-react'; import { toast } from './ui/sonner'; import { isFavorite } from '../utils/favorites'; import { handleKeyboardActivate } from '../utils/a11y'; @@ -14,7 +14,11 @@ interface ChatHeaderProps { channels: Channel[]; config: RadioConfig | null; favorites: Favorite[]; + notificationsSupported: boolean; + notificationsEnabled: boolean; + notificationsPermission: NotificationPermission | 'unsupported'; onTrace: () => void; + onToggleNotifications: () => void; onToggleFavorite: (type: 'channel' | 'contact', id: string) => void; onSetChannelFloodScopeOverride?: (key: string, floodScopeOverride: string) => void; onDeleteChannel: (key: string) => void; @@ -29,7 +33,11 @@ export function ChatHeader({ channels, config, favorites, + notificationsSupported, + notificationsEnabled, + notificationsPermission, onTrace, + onToggleNotifications, onToggleFavorite, onSetChannelFloodScopeOverride, onDeleteChannel, @@ -198,6 +206,35 @@ export function ChatHeader({