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('.');
+}