feat(mobile): Avatar + ConversationRow + relative-time helper
This commit is contained in:
@@ -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 (
|
||||||
|
<View style={[styles.circle, { width: size, height: size, borderRadius: size / 2 }]}>
|
||||||
|
<Text style={[styles.letter, { fontSize: size * 0.45 }]}>{letter}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
circle: {
|
||||||
|
backgroundColor: colors.accentMuted,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
letter: { color: colors.accent, fontWeight: '700' },
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) => [styles.row, pressed && { backgroundColor: colors.surface }]}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<Avatar name={title} />
|
||||||
|
<View style={styles.center}>
|
||||||
|
<View style={styles.titleLine}>
|
||||||
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.time}>{formatRelativeTime(conversation.lastMessageAt)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.preview} numberOfLines={1}>
|
||||||
|
{lastMessagePreview || subtitle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
});
|
||||||
@@ -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('.');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user