/** * Activity (non-comment) timeline row. Mirrors the visual contract of web's * `packages/views/issues/components/issue-detail.tsx:1046-1100`: * * 1. Single-line layout — `[lead icon] [actor name] [verb...truncate] [×N badge?] [time→]` * 2. Contextual lead icon by action: * - status_changed → StatusIcon for the NEW status (details.to) * - priority_changed → PriorityIcon for the NEW priority (details.to) * - due_date_changed → Calendar glyph * - everything else → small ActorAvatar (size 16) * The icon does the recognition work — user sees the new state at a * glance before they read the verb. * 3. Whole row is `text-xs text-muted-foreground` (web parity). Actor name * is `font-medium` but inherits the muted color — activity is supposed * to feel quiet next to comment bubbles. * 4. Time is **relative**, right-aligned (`ml-auto`). Web shows absolute * time in a hover tooltip; mobile has no hover so we just rely on * relative for v1 (long-press → absolute time can be V2). * 5. Coalesce ×N chip when `coalesced_count > 1`, except `task_completed` / * `task_failed` which already bake the count into their phrase. */ import { View } from "react-native"; import Svg, { Line, Rect } from "react-native-svg"; import type { IssuePriority, IssueStatus, TimelineEntry, } from "@multica/core/types"; import { Text } from "@/components/ui/text"; import { StatusIcon } from "@/components/ui/status-icon"; import { PriorityIcon } from "@/components/ui/priority-icon"; import { ActorAvatar } from "@/components/ui/actor-avatar"; import { formatActivity } from "@/lib/format-activity"; import { timeAgo } from "@/lib/time-ago"; import { useActorLookup } from "@/data/use-actor-name"; import { useColorScheme } from "@/lib/use-color-scheme"; import { THEME } from "@/lib/theme"; function CalendarGlyph({ size = 14, stroke, }: { size?: number; stroke: string; }) { return ( ); } function LeadIcon({ entry, mutedFg, }: { entry: TimelineEntry; mutedFg: string; }) { const details = (entry.details ?? {}) as Record; if (entry.action === "status_changed" && details.to) { return ; } if (entry.action === "priority_changed" && details.to) { return ; } if ( entry.action === "due_date_changed" || entry.action === "start_date_changed" ) { return ; } return ( ); } export function ActivityRow({ entry }: { entry: TimelineEntry }) { const { getName } = useActorLookup(); const { colorScheme } = useColorScheme(); const mutedFg = THEME[colorScheme].mutedForeground; const resolveName = ( type: string | null | undefined, id: string | null | undefined, ): string => getName(type as "member" | "agent" | null | undefined, id); const actorName = resolveName(entry.actor_type, entry.actor_id); const verb = formatActivity(entry, resolveName); const showCoalesceBadge = (entry.coalesced_count ?? 1) > 1 && entry.action !== "task_completed" && entry.action !== "task_failed"; return ( {actorName} {verb ? ( {verb} ) : null} {showCoalesceBadge ? ( ×{entry.coalesced_count} ) : null} {timeAgo(entry.created_at)} ); }