import { useState, useEffect, useCallback, useRef, memo, type KeyboardEvent } from 'react' import { invoke } from '@tauri-apps/api/core' import { cn } from '@/lib/utils' import { isTauri, mockInvoke } from '../mock-tauri' import { useDragRegion } from '../hooks/useDragRegion' import type { PulseCommit, PulseFile } from '../types' import { relativeDate } from '../utils/noteListHelpers' import { getLocaleDateLocale, translate, type AppLocale } from '../lib/i18n' import { Plus, Minus, PencilSimple, GitCommit, ArrowSquareOut, FileText, CaretDown, CaretRight, Pulse, } from '@phosphor-icons/react' function tauriCall(command: string, args: Record): Promise { return isTauri() ? invoke(command, args) : mockInvoke(command, args) } interface PulseViewProps { vaultPath: string onOpenNote?: (relativePath: string, commitHash?: string) => void sidebarCollapsed?: boolean onExpandSidebar?: () => void locale?: AppLocale } function formatDateKey(date: Date): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function groupCommitsByDay(commits: PulseCommit[]): Map { const groups = new Map() for (const commit of commits) { const key = formatDateKey(new Date(commit.date * 1000)) const existing = groups.get(key) if (existing) { existing.push(commit) } else { groups.set(key, [commit]) } } return groups } function isToday(dateKey: string): boolean { return dateKey === formatDateKey(new Date()) } function isYesterday(dateKey: string): boolean { return dateKey === formatDateKey(new Date(Date.now() - 86400000)) } function formatDayLabel(dateKey: string, locale: AppLocale): string { if (isToday(dateKey)) return translate(locale, 'pulse.today') if (isYesterday(dateKey)) return translate(locale, 'pulse.yesterday') const date = new Date(`${dateKey}T00:00:00`) const dateLocale = getLocaleDateLocale(locale) return date.toLocaleDateString(dateLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) } const STATUS_ICON = { added: Plus, modified: PencilSimple, deleted: Minus, } as const const STATUS_COLOR = { added: 'var(--accent-green)', modified: 'var(--accent-orange)', deleted: 'var(--destructive)', } as const const PULSE_ROW_FOCUS_CLASS_NAME = 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/70 focus-visible:ring-inset' const PULSE_EDGE_TO_EDGE_ROW_CLASS_NAME = '-mx-4 rounded-none px-4' function SummaryBadges({ added, modified, deleted }: { added: number; modified: number; deleted: number }) { return (
{added > 0 && +{added}} {modified > 0 && ~{modified}} {deleted > 0 && -{deleted}}
) } function handleActivationKey(event: KeyboardEvent, action: () => void) { if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() action() } function FileItem({ file, commitHash, onOpenNote, }: { file: PulseFile commitHash: string onOpenNote?: (path: string, commitHash?: string) => void }) { const Icon = STATUS_ICON[file.status] ?? FileText const color = STATUS_COLOR[file.status] ?? 'var(--muted-foreground)' const handleOpen = useCallback(() => { onOpenNote?.(file.path, commitHash) }, [commitHash, file.path, onOpenNote]) return ( ) } function CommitCard({ commit, locale, onOpenNote, }: { commit: PulseCommit locale: AppLocale onOpenNote?: (path: string, commitHash?: string) => void }) { const [expanded, setExpanded] = useState(false) const Chevron = expanded ? CaretDown : CaretRight const toggleExpanded = useCallback(() => setExpanded((value) => !value), []) return (
{ if (event.target !== event.currentTarget) return handleActivationKey(event, toggleExpanded) }} >
{expanded && commit.files.length > 0 && (
{commit.files.map((file) => ( ))}
)}
) } function DayGroup({ label, commits, locale, onOpenNote }: { label: string commits: PulseCommit[] locale: AppLocale onOpenNote?: (path: string, commitHash?: string) => void }) { const [collapsed, setCollapsed] = useState(false) const Chevron = collapsed ? CaretRight : CaretDown const toggleCollapsed = useCallback(() => setCollapsed((value) => !value), []) return (
handleActivationKey(event, toggleCollapsed)} > {label} ({translate(locale, 'pulse.commitCount', { count: commits.length, label: translate(locale, commits.length === 1 ? 'pulse.commitSingular' : 'pulse.commitPlural'), })})
{!collapsed && commits.map((commit) => ( ))}
) } function PulseHeader({ sidebarCollapsed, onExpandSidebar, locale = 'en', }: Pick) { const { onMouseDown } = useDragRegion() return (
{sidebarCollapsed && onExpandSidebar && ( )} {translate(locale, 'pulse.title')}
) } function EmptyState({ locale = 'en' }: { locale?: AppLocale }) { return (

{translate(locale, 'pulse.noActivity')}

{translate(locale, 'pulse.emptyDescription')}

) } function ErrorState({ message, locale = 'en', onRetry }: { message: string; locale?: AppLocale; onRetry: () => void }) { return (

{message}

) } function PulseFeed({ commits, dayGroups, loading, loadingMore, error, locale, onOpenNote, onRetry, sentinelRef, }: { commits: PulseCommit[] dayGroups: Map loading: boolean loadingMore: boolean error: string | null locale: AppLocale onOpenNote?: (path: string, commitHash?: string) => void onRetry: () => void sentinelRef: React.RefObject }) { if (loading) { return (
{translate(locale, 'pulse.loadingActivity')}
) } if (error) { return } if (commits.length === 0) { return } return ( <> {Array.from(dayGroups.entries()).map(([day, dayCommits]) => ( ))}
{loadingMore && (
{translate(locale, 'pulse.loading')}
)} ) } const PAGE_SIZE = 20 export const PulseView = memo(function PulseView({ vaultPath, onOpenNote, sidebarCollapsed, onExpandSidebar, locale = 'en' }: PulseViewProps) { const [commits, setCommits] = useState([]) const [loading, setLoading] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const [error, setError] = useState(null) const [hasMore, setHasMore] = useState(true) const [skip, setSkip] = useState(0) const sentinelRef = useRef(null) // Initial load const loadInitial = useCallback(async () => { setLoading(true) setError(null) setCommits([]) setSkip(0) setHasMore(true) try { const result = await tauriCall('get_vault_pulse', { vaultPath, limit: PAGE_SIZE, skip: 0 }) setCommits(result) setHasMore(result.length >= PAGE_SIZE) setSkip(result.length) } catch (err) { const msg = typeof err === 'string' ? err : translate(locale, 'pulse.loadError') setError(msg) } finally { setLoading(false) } }, [locale, vaultPath]) // Append next page const loadMore = useCallback(async () => { if (loadingMore || !hasMore) return setLoadingMore(true) try { const result = await tauriCall('get_vault_pulse', { vaultPath, limit: PAGE_SIZE, skip }) setCommits((prev) => [...prev, ...result]) setHasMore(result.length >= PAGE_SIZE) setSkip((s) => s + result.length) } catch { // silently fail for pagination — user can scroll up/retry } finally { setLoadingMore(false) } }, [vaultPath, skip, loadingMore, hasMore]) useEffect(() => { loadInitial() }, [loadInitial]) // Intersection Observer for infinite scroll useEffect(() => { const sentinel = sentinelRef.current if (!sentinel) return const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) loadMore() }, { threshold: 0.1 }, ) observer.observe(sentinel) return () => observer.disconnect() }, [loadMore]) const dayGroups = groupCommitsByDay(commits) return (
) })