diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index a8ab584..9dda994 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -8,6 +8,7 @@ import { useLayoutStore } from '../stores/layout.ts'; import { ServerBar } from './ServerBar.tsx'; import { ChannelList } from './ChannelList.tsx'; import { ConversationList } from './ConversationList.tsx'; +import { NotificationPrompt } from './NotificationPrompt.tsx'; import { MemberList } from './MemberList.tsx'; import { VoicePanel } from './VoicePanel.tsx'; import { ServerSettingsModal } from './ServerSettingsModal.tsx'; @@ -223,6 +224,7 @@ export function Layout() { {new Date().toISOString().slice(0, 10)} + ); } diff --git a/web/src/components/NotificationPrompt.tsx b/web/src/components/NotificationPrompt.tsx new file mode 100644 index 0000000..4a4ab49 --- /dev/null +++ b/web/src/components/NotificationPrompt.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { usePushStore } from '../stores/push.ts'; + +export function NotificationPrompt() { + const { isSupported, permission, isSubscribed, subscribe } = usePushStore(); + const [dismissed, setDismissed] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (localStorage.getItem('push-prompt-dismissed') === 'true') { + setDismissed(true); + } + }, []); + + // Don't show if not supported, already subscribed, permission resolved, or dismissed + if (!isSupported || isSubscribed || permission === 'granted' || permission === 'denied' || dismissed) { + return null; + } + + const handleEnable = async () => { + setLoading(true); + try { + await subscribe(); + } catch { + // user denied or error + } finally { + setLoading(false); + } + }; + + const handleDismiss = () => { + setDismissed(true); + localStorage.setItem('push-prompt-dismissed', 'true'); + }; + + return ( +
+
🔔
+
+

Enable notifications?

+

Get notified about new messages

+
+
+ + +
+
+ ); +} diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts index b0f0506..459d132 100644 --- a/web/src/stores/auth.ts +++ b/web/src/stores/auth.ts @@ -72,10 +72,12 @@ interface AuthState { } // ponytail: auto-subscribe to push notifications on auth +// Only fires when permission is already 'granted' (re-subscribe on session restore). +// iOS requires a user gesture for the initial permission prompt — that's handled by NotificationPrompt. function autoSubscribePush() { import("../stores/push.ts").then(({ usePushStore }) => { const ps = usePushStore.getState(); - if (ps.isSupported && Notification.permission !== 'denied' && !ps.isSubscribed) { + if (ps.isSupported && Notification.permission === 'granted' && !ps.isSubscribed) { ps.subscribe(); } });