- 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
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useEffect } from 'react';
|
|
import { useSWRConfig } from 'swr';
|
|
import useSWRInfinite from 'swr/infinite';
|
|
import { notificationPath, notificationText, timeAgo, type NotificationListDTO } from '@musicshare/shared';
|
|
import { Avatar } from '@/components/Avatar';
|
|
import { client, useMe } from '@/lib/api';
|
|
|
|
/** Reactions, comments, new followers and songs sent to you, newest first. Opening it marks them read. */
|
|
export default function NotificationsPage() {
|
|
const { me, loggedOut } = useMe();
|
|
const { mutate } = useSWRConfig();
|
|
const { data, size, setSize, isLoading } = useSWRInfinite<NotificationListDTO>(
|
|
(i, prev: NotificationListDTO | null) => (!me || (i > 0 && !prev?.nextCursor) ? null : ['notifications', prev?.nextCursor ?? null]),
|
|
([, cursor]: [string, string | null]) => client.notifications(cursor),
|
|
{ revalidateOnFocus: false },
|
|
);
|
|
const items = data?.flatMap((p) => p.notifications) ?? [];
|
|
const unread = data?.[0]?.unread ?? 0;
|
|
|
|
// Seen once you've looked: clear the bell (rows stay highlighted until you come back).
|
|
useEffect(() => {
|
|
if (unread) client.markNotificationsRead().then(() => mutate('/notifications'), () => {});
|
|
}, [unread, mutate]);
|
|
|
|
if (loggedOut) return <p className="muted"><Link href="/login">Log in</Link> to see your notifications.</p>;
|
|
return (
|
|
<div className="stack">
|
|
<h1>Notifications</h1>
|
|
{items.length === 0 && !isLoading && (
|
|
<p className="muted">Nothing yet. Reactions, new followers and songs sent to you show up here.</p>
|
|
)}
|
|
<ul className="notifications">
|
|
{items.map((n) => {
|
|
const { title, body } = notificationText(n);
|
|
return (
|
|
<li key={n.id} className={n.read ? '' : 'unread'}>
|
|
<Link href={notificationPath(n)} className="notification">
|
|
<Avatar user={n.actor} size={40} />
|
|
<span className="notification-text">
|
|
<strong>{title}</strong>
|
|
<span>{body}</span>
|
|
<span className="muted">{timeAgo(n.createdAt)}</span>
|
|
</span>
|
|
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
{data?.at(-1)?.nextCursor && (
|
|
<button className="btn" onClick={() => setSize(size + 1)}>
|
|
Show more
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|