Comments on plays

- 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
This commit is contained in:
2026-10-15 13:32:14 -04:00
co-authored by Claude Opus 5.5
parent 31aed8dec4
commit 4402171f46
33 changed files with 2850 additions and 36 deletions
+14 -3
View File
@@ -17,11 +17,22 @@ describe('notifications', () => {
});
it('links songs to the song and follows to the person', () => {
expect(notificationPath({ type: 'share', actor, track })).toBe('/t/t1');
expect(notificationPath({ type: 'follow', actor, track: null })).toBe('/u/bob');
expect(notificationPath({ type: 'share', actor, track, listenId: null })).toBe('/t/t1');
expect(notificationPath({ type: 'follow', actor, track: null, listenId: null })).toBe('/u/bob');
});
it('opens the play for comments, replies and reactions', () => {
expect(notificationPath({ type: 'comment', actor, track, listenId: 'l1' })).toBe('/play/l1');
expect(notificationPath({ type: 'reaction', actor, track, listenId: 'l1' })).toBe('/play/l1');
// The play was trimmed from history: fall back to the song.
expect(notificationPath({ type: 'reaction', actor, track, listenId: null })).toBe('/t/t1');
expect(notificationText({ type: 'reply', actor, track, emoji: null, message: 'same' })).toEqual({
title: 'Bob replied on Teardrop by Massive Attack',
body: 'same',
});
});
it('treats missing settings as on', () => {
expect(notificationPrefs({ follow: false })).toEqual({ reaction: true, follow: false, share: true });
expect(notificationPrefs({ follow: false })).toEqual({ reaction: true, comment: true, reply: true, follow: false, share: true });
});
});