Notifications and sending songs to friends

Reactions to your songs, new followers, and songs sent to you are now
notifications: a bell on the feed (app and website) with an unread
count that updates live, and a list that marks them read. "Send ✉" on
a song sends it to people who follow you, with an optional note.
Settings picks which kinds to be notified about.

Push delivery uses no third-party accounts:
- Android: UnifiedPush through a distributor app (ntfy), pointed at
  our own ntfy server (docker-compose.ntfy.yml). Works the same in
  the F-Droid build; no Firebase.
- Browsers: standard Web Push from the existing service worker.
Both use Web Push encryption with a VAPID key pair the server makes
and keeps in the database. Push addresses are checked like any other
user-supplied URL. Setup is in docs/PUSH.md; the privacy page covers
the new data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 22:14:34 -04:00
co-authored by Claude Opus 5.5
parent 3b39910f13
commit f112a3515c
55 changed files with 3783 additions and 22 deletions
+5
View File
@@ -10,6 +10,7 @@ import { Body, Button, Title } from '@/components/ui';
import { api } from '@/lib/api';
import { appleMusicAvailable, useAppleMusicLive } from '@/lib/appleMusic';
import { syncDevice } from '@/lib/nowPlayingReader';
import { syncPush } from '@/lib/push';
import { SessionProvider, useSession } from '@/lib/session';
import { colors, space } from '@/lib/theme';
@@ -45,6 +46,7 @@ function AppNavigator() {
// Keep the Android listener pointed at this account (no-op elsewhere).
useEffect(() => {
if (signedIn) syncDevice().catch(() => {});
if (signedIn) syncPush().catch(() => {});
}, [signedIn]);
if (!ready) return null;
@@ -99,6 +101,9 @@ function AppNavigator() {
<Stack.Screen name="change-password" options={{ title: 'Change password', presentation: 'modal' }} />
<Stack.Screen name="change-email" options={{ title: 'Change email', presentation: 'modal' }} />
<Stack.Screen name="scan" options={{ title: 'Scan a code', presentation: 'modal' }} />
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
<Stack.Screen name="t/[id]" options={{ title: '' }} />
<Stack.Screen name="send/[trackId]" options={{ title: 'Send to friends', presentation: 'modal' }} />
</Stack.Protected>
<Stack.Protected guard={!signedIn}>
<Stack.Screen name="login" options={{ headerShown: false }} />