// apps/mobile/app/(tabs)/history.tsx // History tab — transcription records list // Design ref: docs/v3/designs/history.html import { useEffect, useState, useCallback } from 'react' import { View, FlatList, StyleSheet, ActivityIndicator, RefreshControl, Pressable } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { MetalCard, PhosphorText, Led, Header, FilterChip, AppStatusBar, d3roNativePalette } from '@d3ro/ui-native' import { useI18n } from '@d3ro/i18n' import { supabase, isSupabaseConfigured } from '../../lib/supabase' interface HistoryEntry { id: string original_text: string | null polished_text: string | null mode: string status: string created_at: string stt_model: string | null word_count: number | null } type FilterType = 'all' | 'favorites' | 'processing' export default function HistoryScreen(): React.ReactElement { const insets = useSafeAreaInsets() const { t, formatTime, formatRelativeDate } = useI18n() const [entries, setEntries] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [filter, setFilter] = useState('all') const load = useCallback(async (): Promise => { if (!isSupabaseConfigured()) { setLoading(false) return } try { const query = supabase .from('history') .select('id, original_text, polished_text, mode, status, created_at, stt_model, word_count') .order('created_at', { ascending: false }) .limit(50) const { data, error } = await query if (!error && data) { setEntries(data as HistoryEntry[]) } } finally { setLoading(false) setRefreshing(false) } }, []) useEffect(() => { void load() }, [load]) // Group entries by date const grouped = groupByDate(entries, formatRelativeDate) if (loading) { return ( ) } return (
} paddingTop={insets.top} /> {/* Filter Chips */} setFilter('all')} /> setFilter('favorites')} /> setFilter('processing')} /> item.id} contentContainerStyle={entries.length === 0 ? styles.center : styles.listContent} refreshControl={ { setRefreshing(true); void load() }} tintColor={d3roNativePalette.accent.amber} /> } ListEmptyComponent={ {t('mobile.hist.empty')} } renderItem={({ item }) => { if (item.type === 'header') { return ( {item.label} ) } const entry = item.entry const isOld = !isToday(entry.created_at) return ( {formatTime(new Date(entry.created_at).getTime())} {entry.word_count != null && ( {entry.word_count} W )} {entry.polished_text ?? entry.original_text ?? '(empty)'} {entry.stt_model?.toUpperCase() ?? 'LOCAL'} ) }} ListFooterComponent={} /> ) } // Helpers interface GroupedItem { id: string type: 'header' | 'entry' label?: string entry: HistoryEntry } function isToday(dateStr: string): boolean { const d = new Date(dateStr) const now = new Date() return d.toDateString() === now.toDateString() } function groupByDate( entries: HistoryEntry[], formatRelativeDate: (ts: number) => string ): GroupedItem[] { const result: GroupedItem[] = [] let lastDate = '' for (const entry of entries) { const dateLabel = formatRelativeDate(new Date(entry.created_at).getTime()) if (dateLabel !== lastDate) { result.push({ id: `header-${dateLabel}`, type: 'header', label: dateLabel, entry }) lastDate = dateLabel } result.push({ id: entry.id, type: 'entry', entry }) } return result } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: d3roNativePalette.bg.app }, center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, backgroundColor: d3roNativePalette.bg.app }, filters: { flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 12, gap: 12, borderBottomWidth: 1, borderBottomColor: d3roNativePalette.border.subtle }, listContent: { padding: 20, paddingBottom: 120 }, emptyText: { textAlign: 'center' }, dateHeader: { letterSpacing: 3, marginBottom: 8, marginTop: 16 }, card: { marginBottom: 12 }, cardOld: { opacity: 0.7 }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, cardHeaderLeft: { flexDirection: 'row', alignItems: 'center' }, timeLabel: { marginLeft: 8 }, wordBadge: { backgroundColor: d3roNativePalette.accent.amberDim, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4 }, transcriptText: { marginBottom: 8 }, cardMeta: { flexDirection: 'row', gap: 12 } })