import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import type { TimelineEvent } from '../../../shared/types'; import { fetchTimeline, markAllRead, markRead } from '../api'; import { relativeTime, shortRepo } from '../format'; const GROUP_WINDOW_MS = 10 * 60 * 1000; interface Group { key: string; actor: string; repo: string; prNumber: number; prTitle: string; events: TimelineEvent[]; } function groupEvents(events: TimelineEvent[]): Group[] { const groups: Group[] = []; for (const e of events) { const last = groups[groups.length - 1]; const lastEvent = last?.events[last.events.length - 1]; if ( last && lastEvent && last.actor === e.actor && last.repo === e.repo && last.prNumber === e.prNumber && Date.parse(lastEvent.createdAt) - Date.parse(e.createdAt) < GROUP_WINDOW_MS ) { last.events.push(e); } else { groups.push({ key: e.id, actor: e.actor, repo: e.repo, prNumber: e.prNumber, prTitle: e.prTitle, events: [e], }); } } return groups; } function KindLabel({ event }: { event: TimelineEvent }) { if (event.kind === 'review') { const cls = event.reviewState === 'APPROVED' ? 'approved' : event.reviewState === 'CHANGES_REQUESTED' ? 'changes' : ''; const label = event.reviewState === 'APPROVED' ? 'approved' : event.reviewState === 'CHANGES_REQUESTED' ? 'requested changes' : 'reviewed'; return {label}; } if (event.kind === 'review_comment') { return ( commented{event.path ? ' on ' : ''} {event.path && {event.path.split('/').pop()}} ); } return commented; } export default function Timeline() { const [unreadOnly, setUnreadOnly] = useState(false); const queryClient = useQueryClient(); const query = useInfiniteQuery({ queryKey: ['timeline', unreadOnly], queryFn: ({ pageParam }) => fetchTimeline({ cursor: pageParam, unreadOnly }), initialPageParam: undefined as string | undefined, getNextPageParam: (page) => page.nextCursor ?? undefined, }); const invalidate = () => { void queryClient.invalidateQueries({ queryKey: ['timeline'] }); void queryClient.invalidateQueries({ queryKey: ['state'] }); }; const readMutation = useMutation({ mutationFn: ({ id, read }: { id: string; read: boolean }) => markRead(id, read), onSettled: invalidate, }); const readAllMutation = useMutation({ mutationFn: markAllRead, onSettled: invalidate }); const events = query.data?.pages.flatMap((p) => p.events) ?? []; const groups = groupEvents(events); const openEvent = (e: TimelineEvent) => { if (!e.read) readMutation.mutate({ id: e.id, read: true }); window.open(e.url, '_blank', 'noopener'); }; return ( <>

Activity

{query.isLoading &&
Loading…
} {!query.isLoading && groups.length === 0 && (
{unreadOnly ? 'Nothing unread.' : 'No activity yet.'}
)} {groups.map((group) => (
{group.actor} {shortRepo(group.repo)}#{group.prNumber} · {group.prTitle} {relativeTime(group.events[0]!.createdAt)}
{group.events.map((e) => (
openEvent(e)} >
))}
))} {query.hasNextPage && ( )}
); }