- Anyone who can see a play can read its comments; the player and the people they follow can write. The author, the player and admins can delete; comments can be reported (admins see the text). - Notifications: "commented on" for the player, "replied on" for earlier commenters, each with its own setting. Reaction and comment notifications now open the play. - A 💬 chip with the count next to reactions on the feed and recent plays, opening the thread on the app (/play/[id]) and the website. Threads refresh every few seconds while open. - Blocking removes comments between the two people. Plays with comments survive history trimming. - Privacy page updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import type { TrackDTO, UserSummaryDTO } from './dto';
|
|
|
|
export const NOTIFICATION_TYPES = ['reaction', 'comment', 'reply', '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',
|
|
comment: 'Comments on your songs',
|
|
reply: 'Replies where you commented',
|
|
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,
|
|
comment: stored?.comment ?? true,
|
|
reply: stored?.reply ?? true,
|
|
follow: stored?.follow ?? true,
|
|
share: stored?.share ?? true,
|
|
});
|
|
|
|
export interface NotificationDTO {
|
|
id: string;
|
|
type: NotificationType;
|
|
actor: UserSummaryDTO;
|
|
track: TrackDTO | null;
|
|
/** The play a reaction or comment was on, while it exists. */
|
|
listenId: string | 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 'comment':
|
|
return { title: `${who} commented on ${song}`, body: n.message ?? '' };
|
|
case 'reply':
|
|
return { title: `${who} replied on ${song}`, body: n.message ?? '' };
|
|
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' | 'listenId'>): string {
|
|
if ((n.type === 'comment' || n.type === 'reply' || n.type === 'reaction') && n.listenId) return `/play/${n.listenId}`;
|
|
if ((n.type === 'share' || n.type === 'reaction') && n.track) return `/t/${n.track.id}`;
|
|
return `/u/${n.actor.username}`;
|
|
}
|