import {useEffect, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {Link} from 'react-router-dom'; import {ChevronDown, Loader2} from 'lucide-react'; import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {cn} from '@/lib/utils'; import PageContainer from '../components/PageContainer.jsx'; import {formatDate} from '../utils/format.js'; import {profileGameHref, profileHref} from '../utils/people.js'; import {digestParts, fetchInbox, fetchInboxDetails, groupDetails, markInboxRead} from '../utils/inbox.js'; const PSN_KINDS = ['PSN_EXPIRING', 'PSN_EXPIRED']; const ACHIEVEMENTS_SHOWN = 5; /** * The newest notes about the people the user follows and about the PlayStation link. Opening the page marks them read. */ export default function InboxPage() { const {t, i18n} = useTranslation(); const [notes, setNotes] = useState([]); const [status, setStatus] = useState('loading'); const [open, setOpen] = useState(null); // note id -> {status: 'loading' | 'error' | 'ready', results} const [details, setDetails] = useState({}); const [showAll, setShowAll] = useState({}); useEffect(() => { (async () => { const {ok, status: code, data} = await fetchInbox(); if (code === 401) return; if (!ok) { setStatus('error'); return; } setNotes(data.results); setStatus('ready'); // A failed mark is silent: the notes show as unread again on the next visit. if (data.unread > 0) markInboxRead(); })(); }, []); const text = (note) => { if (PSN_KINDS.includes(note.kind)) return t(`INBOX.${note.kind}`); const name = note.displayName ?? t('FOLLOW.PRIVATE_USER'); if (note.kind === 'VISIBILITY') return t('INBOX.VISIBILITY', {name}); return t('INBOX.DIGEST', {name, parts: partsText(note)}); }; const partsText = (note) => digestParts(note).map(({key, count}) => t(`INBOX.PART_${key}`, {count})).join(', '); // A link in a button is invalid, so the profile link sits next to the toggle button. const noteLine = (note) => { const linked = note.kind === 'DIGEST' && note.profileName; const label = linked ? t('INBOX.DIGEST_PARTS', {parts: partsText(note)}) : text(note); const line = note.kind === 'DIGEST' && note.details ? ( ) : label; if (!linked) return line; return <>{note.displayName}: {line}>; }; const toggle = async (id) => { if (open === id) { setOpen(null); return; } setOpen(id); if (details[id]) return; setDetails((current) => ({...current, [id]: {status: 'loading', results: []}})); const {ok, status, data} = await fetchInboxDetails(id); setDetails((current) => ({...current, [id]: ok || status === 404 ? {status: 'ready', results: data?.results ?? []} : {status: 'error', results: []}})); }; const gameLabel = (note, {gameName, appid}) => ( <> {appid == null ? gameName : {gameName}} {appid != null && note.profileName && ( {t('INBOX.ON_PROFILE')} )} > ); const gameLines = (group, gameGroup, note) => { const {gameName, items} = gameGroup; if (group.kind === 'STATUS') { return items.map((item, i) => (
{t('INBOX.DETAILS_ERROR')}
; const groups = groupDetails(entry.results); if (groups.length === 0) return{t('INBOX.DETAILS_GONE')}
; return groups.map((group) => ({t('INBOX.LOAD_ERROR')}
} {status === 'ready' && notes.length === 0 &&{t('INBOX.EMPTY')}
} {status === 'ready' && notes.length > 0 && ({formatDate(note.createdAt, i18n.language, {dateStyle: 'medium'})}
{noteLine(note)} {PSN_KINDS.includes(note.kind) && ( <> {t('INBOX.OPEN_SETTINGS')}> )}
{open === note.id &&