From bee273ab569af1fcdff1d943a33924a27eb6fcf0 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Tue, 10 Mar 2026 18:47:03 -0700 Subject: [PATCH] Add notifications --- frontend/src/App.tsx | 29 ++- frontend/src/components/ChatHeader.tsx | 39 +++- frontend/src/components/ConversationPane.tsx | 16 ++ frontend/src/components/RepeaterDashboard.tsx | 39 +++- frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useBrowserNotifications.ts | 207 ++++++++++++++++++ frontend/src/hooks/useRealtimeAppState.ts | 14 +- .../src/test/chatHeaderKeyVisibility.test.tsx | 24 ++ frontend/src/test/conversationPane.test.tsx | 4 + frontend/src/test/repeaterDashboard.test.tsx | 19 ++ .../src/test/useBrowserNotifications.test.ts | 151 +++++++++++++ frontend/src/test/useRealtimeAppState.test.ts | 2 + 12 files changed, 539 insertions(+), 6 deletions(-) create mode 100644 frontend/src/hooks/useBrowserNotifications.ts create mode 100644 frontend/src/test/useBrowserNotifications.test.ts 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({