diff --git a/apps/mobile/components/Avatar.tsx b/apps/mobile/components/Avatar.tsx new file mode 100644 index 0000000..056824e --- /dev/null +++ b/apps/mobile/components/Avatar.tsx @@ -0,0 +1,23 @@ +import { StyleSheet, Text, View } from 'react-native'; + +import { colors } from '../theme/colors'; + +// Initial-letter avatar circle. Phase 2 will swap in an image-aware +// version that prefers profile.avatarUrl when present. +export function Avatar({ name, size = 40 }: { name: string; size?: number }) { + const letter = (name.trim().charAt(0) || '?').toUpperCase(); + return ( + + {letter} + + ); +} + +const styles = StyleSheet.create({ + circle: { + backgroundColor: colors.accentMuted, + alignItems: 'center', + justifyContent: 'center', + }, + letter: { color: colors.accent, fontWeight: '700' }, +}); diff --git a/apps/mobile/components/ConversationRow.tsx b/apps/mobile/components/ConversationRow.tsx new file mode 100644 index 0000000..4b1ff32 --- /dev/null +++ b/apps/mobile/components/ConversationRow.tsx @@ -0,0 +1,58 @@ +import type { ConversationSummary } from '@chat-app/shared/chat'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; + +import { formatRelativeTime } from '../lib/timeFormat'; +import { colors } from '../theme/colors'; +import { Avatar } from './Avatar'; + +interface Props { + conversation: ConversationSummary; + lastMessagePreview: string; + onPress: () => void; +} + +export function ConversationRow({ conversation, lastMessagePreview, onPress }: Props) { + const title = + conversation.type === 'group' + ? (conversation.name ?? 'Gruppe') + : (conversation.peer?.displayName ?? '—'); + const subtitle = + conversation.type === 'group' + ? conversation.members.length + ' Mitglieder' + : '@' + (conversation.peer?.username ?? ''); + + return ( + [styles.row, pressed && { backgroundColor: colors.surface }]} + onPress={onPress} + > + + + + + {title} + + {formatRelativeTime(conversation.lastMessageAt)} + + + {lastMessagePreview || subtitle} + + + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + gap: 12, + }, + center: { flex: 1, minWidth: 0 }, + titleLine: { flexDirection: 'row', alignItems: 'baseline', gap: 8 }, + title: { color: colors.text, fontSize: 15, fontWeight: '600', flex: 1 }, + time: { color: colors.textDim, fontSize: 11 }, + preview: { color: colors.textMuted, fontSize: 13, marginTop: 2 }, +}); diff --git a/apps/mobile/lib/timeFormat.ts b/apps/mobile/lib/timeFormat.ts new file mode 100644 index 0000000..f1df43a --- /dev/null +++ b/apps/mobile/lib/timeFormat.ts @@ -0,0 +1,18 @@ +// Minimal relative-time formatter for chat lists. Matches the desktop's +// terse style ("Vor 5 Min", "Gestern", "12.05."). +export function formatRelativeTime(iso: string | null): string { + if (!iso) return ''; + const then = new Date(iso).getTime(); + if (!Number.isFinite(then)) return ''; + const diffSec = (Date.now() - then) / 1000; + if (diffSec < 60) return 'Gerade eben'; + if (diffSec < 3600) return 'Vor ' + Math.floor(diffSec / 60) + ' Min'; + if (diffSec < 86400) return 'Vor ' + Math.floor(diffSec / 3600) + ' Std'; + if (diffSec < 86400 * 2) return 'Gestern'; + const d = new Date(iso); + return [ + String(d.getDate()).padStart(2, '0'), + String(d.getMonth() + 1).padStart(2, '0'), + String(d.getFullYear()).slice(-2), + ].join('.'); +}