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:
@@ -0,0 +1,61 @@
|
||||
import type { TrackDTO, UserSummaryDTO } from './dto';
|
||||
|
||||
export const NOTIFICATION_TYPES = ['reaction', 'follow', 'share'] as const;
|
||||
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
|
||||
|
||||
/** Which notifications get pushed to your phone/browser. */
|
||||
export type NotificationPrefs = Record<NotificationType, boolean>;
|
||||
|
||||
export const NOTIFICATION_PREF_LABELS: Record<NotificationType, string> = {
|
||||
reaction: 'Reactions to your songs',
|
||||
follow: 'New followers',
|
||||
share: 'Songs sent to you',
|
||||
};
|
||||
|
||||
/** Missing settings mean on. */
|
||||
export const notificationPrefs = (stored: Partial<NotificationPrefs> | null | undefined): NotificationPrefs => ({
|
||||
reaction: stored?.reaction ?? true,
|
||||
follow: stored?.follow ?? true,
|
||||
share: stored?.share ?? true,
|
||||
});
|
||||
|
||||
export interface NotificationDTO {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
actor: UserSummaryDTO;
|
||||
track: TrackDTO | null;
|
||||
emoji: string | null;
|
||||
message: string | null;
|
||||
read: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface NotificationListDTO {
|
||||
notifications: NotificationDTO[];
|
||||
unread: number;
|
||||
/** Pass back as ?cursor= for older ones; null at the end. */
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
/** What a push or a row in the list says. */
|
||||
export function notificationText(n: Pick<NotificationDTO, 'type' | 'actor' | 'track' | 'emoji' | 'message'>): {
|
||||
title: string;
|
||||
body: string;
|
||||
} {
|
||||
const who = n.actor.displayName ?? n.actor.username;
|
||||
const song = n.track ? `${n.track.title} by ${n.track.artist}` : 'a song';
|
||||
switch (n.type) {
|
||||
case 'reaction':
|
||||
return { title: `${who} reacted ${n.emoji ?? ''}`.trim(), body: `to ${song}` };
|
||||
case 'follow':
|
||||
return { title: `${who} followed you`, body: 'Follow back to see what they play.' };
|
||||
case 'share':
|
||||
return { title: `${who} sent you a song`, body: n.message ? `${song}: “${n.message}”` : song };
|
||||
}
|
||||
}
|
||||
|
||||
/** Where tapping it goes, as an app/web path. */
|
||||
export function notificationPath(n: Pick<NotificationDTO, 'type' | 'actor' | 'track'>): string {
|
||||
if ((n.type === 'share' || n.type === 'reaction') && n.track) return `/t/${n.track.id}`;
|
||||
return `/u/${n.actor.username}`;
|
||||
}
|
||||
Reference in New Issue
Block a user