/** * ChatTile — одна строка в списке чатов на главной (Messages screen). * * Layout: * [avatar 44] [name (+verified) (+kind-icon)] [time] * [last-msg preview] [unread pill] * * Kind-icon — мегафон для channel, 👥 для group, ничего для direct. * Verified checkmark — если у контакта есть @username. * Online-dot на аватарке — только для direct-чатов с x25519 ключом. */ import React from 'react'; import { View, Text, Pressable } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import type { Contact, Message } from '@/lib/types'; import { Avatar } from '@/components/Avatar'; import { formatWhen } from '@/lib/dates'; import { useStore } from '@/lib/store'; function previewText(s: string, max = 50): string { return s.length <= max ? s : s.slice(0, max).trimEnd() + '…'; } /** * Текстовое превью последнего сообщения. Если у сообщения нет текста * (только вложение) — возвращаем маркер с иконкой названием типа: * "🖼 Photo" / "🎬 Video" / "🎙 Voice" / "📎 File" * Если текст есть — он используется; если есть и то и другое, префикс * добавляется перед текстом. */ function lastPreview(m: Message): string { const emojiByKind = { image: '🖼', video: '🎬', voice: '🎙', file: '📎', } as const; const labelByKind = { image: 'Photo', video: 'Video', voice: 'Voice message', file: 'File', } as const; const text = m.text.trim(); if (m.attachment) { const prefix = `${emojiByKind[m.attachment.kind]} ${labelByKind[m.attachment.kind]}`; return text ? `${prefix} ${previewText(text, 40)}` : prefix; } return previewText(text); } function shortAddr(a: string, n = 5): string { if (!a) return '—'; return a.length <= n * 2 + 1 ? a : `${a.slice(0, n)}…${a.slice(-n)}`; } function displayName(c: Contact): string { return c.username ? `@${c.username}` : c.alias ?? shortAddr(c.address); } export interface ChatTileProps { contact: Contact; lastMessage: Message | null; onPress: () => void; } export function ChatTile({ contact: c, lastMessage, onPress }: ChatTileProps) { const name = displayName(c); const last = lastMessage; // Визуальный маркер типа чата. const kindIcon: React.ComponentProps['name'] | null = c.kind === 'group' ? 'people' : null; // Unread берётся из runtime-store'а (инкрементится в useGlobalInbox, // обнуляется при открытии чата). Fallback на c.unread для legacy seed. const storeUnread = useStore(s => s.unreadByContact[c.address] ?? 0); const unreadCount = storeUnread || (c.unread ?? 0); const unread = unreadCount > 0 ? unreadCount : null; return ( ({ backgroundColor: pressed ? '#0a0a0a' : 'transparent', })} > {/* Первая строка: [kind-icon] name [verified] ··· time */} {kindIcon && ( )} {name} {c.username && ( )} {last && ( {formatWhen(last.timestamp)} )} {/* Вторая строка: [✓✓ mine-seen] preview ··· [unread] */} {last?.mine && ( )} {last ? lastPreview(last) : c.x25519Pub ? 'Tap to start encrypted chat' : 'Waiting for identity…'} {unread !== null && ( {unread > 99 ? '99+' : unread} )} ); }