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
@@ -0,0 +1,44 @@
import { router } from 'expo-router';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import useSWR from 'swr';
import type { NotificationListDTO } from '@musicshare/shared';
import { colors } from '@/lib/theme';
/** Bell with the unread count; opens your notifications. */
export function ActivityBell() {
const { data } = useSWR<NotificationListDTO>('/notifications');
const unread = data?.unread ?? 0;
return (
<Pressable
onPress={() => router.push('/notifications')}
hitSlop={10}
accessibilityRole="button"
accessibilityLabel={unread ? `Notifications, ${unread} new` : 'Notifications'}
style={styles.bell}>
<Text style={styles.icon}>🔔</Text>
{unread > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{unread > 99 ? '99+' : unread}</Text>
</View>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
bell: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.surface2 },
icon: { fontSize: 18 },
badge: {
position: 'absolute',
top: -2,
right: -4,
minWidth: 18,
height: 18,
paddingHorizontal: 4,
borderRadius: 9,
backgroundColor: colors.danger,
alignItems: 'center',
justifyContent: 'center',
},
badgeText: { color: '#fff', fontSize: 11, fontWeight: '700' },
});
+6
View File
@@ -1,4 +1,5 @@
import { Image } from 'expo-image';
import { router } from 'expo-router';
import { Linking, Pressable, Share, StyleSheet, Text, View } from 'react-native';
import { SERVICE_LABELS, openUrl, shareText, trackUrl, viewerServices, type StreamingService, type TrackDTO } from '@musicshare/shared';
import { WEB_URL } from '@/lib/config';
@@ -60,6 +61,11 @@ export function ServiceLinks({ track, preferred }: { track: TrackDTO; preferred:
</Pressable>
);
})}
{me && (
<Pressable onPress={() => router.push(`/send/${track.id}`)} hitSlop={6} accessibilityRole="button" accessibilityLabel="Send to friends">
<Text style={[styles.service, styles.share]}>Send ✉</Text>
</Pressable>
)}
<Pressable onPress={() => shareTrack(track, me?.username)} hitSlop={6} accessibilityRole="button" accessibilityLabel="Share this song">
<Text style={[styles.service, styles.share]}>Share ↗</Text>
</Pressable>