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
+6 -1
View File
@@ -1,4 +1,5 @@
import type { Entitlements, Plan } from './plans';
import type { NotificationPrefs } from './notifications';
import type { NowPlayingProvider, StreamingService } from './services';
export interface TrackDTO {
@@ -28,6 +29,7 @@ export interface MeDTO extends UserSummaryDTO {
preferredService: StreamingService | null;
/** The services they use; null until chosen, meaning all of them. */
services: StreamingService[] | null;
notifyPrefs: NotificationPrefs;
role: 'user' | 'admin';
/** When onboarding was finished; null means show it. */
onboardedAt: string | null;
@@ -161,4 +163,7 @@ export interface TrackSearchResultDTO {
}
/** Messages pushed over the realtime websocket. */
export type RealtimeMessage = { type: 'now_playing'; data: NowPlayingDTO };
export type RealtimeMessage =
| { type: 'now_playing'; data: NowPlayingDTO }
/** Something new in the viewer's notifications. */
| { type: 'notification'; data: { unread: number } };