/** * ShareSheet — bottom-sheet picker that forwards a feed post into one * (or several) chats. Opens when the user taps the share icon on a * PostCard. * * Design notes * ------------ * - Single modal component, managed by the parent via `visible` + * `onClose`. Parent passes the `post` it wants to share. * - Multi-select: the user can tick several contacts at once and hit * "Отправить". Fits the common "share with a couple of friends" * flow better than one-at-a-time. * - Only contacts with an x25519 key show up — those are the ones we * can actually encrypt for. An info note explains absent contacts. * - Search: typing filters the list by username / alias / address * prefix. Useful once the user has more than a screenful of * contacts. */ import React, { useMemo, useState } from 'react'; import { View, Text, Pressable, Modal, FlatList, TextInput, ActivityIndicator, Alert, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Avatar } from '@/components/Avatar'; import { useStore } from '@/lib/store'; import type { Contact } from '@/lib/types'; import type { FeedPostItem } from '@/lib/feed'; import { forwardPostToContacts } from '@/lib/forwardPost'; export interface ShareSheetProps { visible: boolean; post: FeedPostItem | null; onClose: () => void; } export function ShareSheet({ visible, post, onClose }: ShareSheetProps) { const insets = useSafeAreaInsets(); const contacts = useStore(s => s.contacts); const keyFile = useStore(s => s.keyFile); const [query, setQuery] = useState(''); const [picked, setPicked] = useState>(new Set()); const [sending, setSending] = useState(false); const available = useMemo(() => { const q = query.trim().toLowerCase(); const withKeys = contacts.filter(c => !!c.x25519Pub); if (!q) return withKeys; return withKeys.filter(c => (c.username ?? '').toLowerCase().includes(q) || (c.alias ?? '').toLowerCase().includes(q) || c.address.toLowerCase().startsWith(q), ); }, [contacts, query]); const toggle = (address: string) => { setPicked(prev => { const next = new Set(prev); if (next.has(address)) next.delete(address); else next.add(address); return next; }); }; const doSend = async () => { if (!post || !keyFile) return; const targets = contacts.filter(c => picked.has(c.address)); if (targets.length === 0) return; setSending(true); try { const { ok, failed } = await forwardPostToContacts({ post, contacts: targets, keyFile, }); if (failed > 0) { Alert.alert('Готово', `Отправлено в ${ok} из ${ok + failed} чат${plural(ok + failed)}.`); } // Close + reset regardless — done is done. setPicked(new Set()); setQuery(''); onClose(); } catch (e: any) { Alert.alert('Не удалось', String(e?.message ?? e)); } finally { setSending(false); } }; const closeAndReset = () => { setPicked(new Set()); setQuery(''); onClose(); }; return ( {/* Dim backdrop — tap to dismiss */} {/* Sheet body — stopPropagation so inner taps don't dismiss */} e.stopPropagation?.()} style={{ backgroundColor: '#0a0a0a', borderTopLeftRadius: 22, borderTopRightRadius: 22, paddingTop: 10, paddingBottom: Math.max(insets.bottom, 10) + 10, maxHeight: '78%', borderTopWidth: 1, borderTopColor: '#1f1f1f', }} > {/* Drag handle */} {/* Title row */} Поделиться постом {/* Search */} {query.length > 0 && ( setQuery('')} hitSlop={6}> )} {/* Contact list */} c.address} renderItem={({ item }) => ( toggle(item.address)} /> )} ListEmptyComponent={ {query.length > 0 ? 'Нет контактов по такому запросу' : 'Контакты с ключами шифрования отсутствуют'} } /> {/* Send button */} ({ alignItems: 'center', justifyContent: 'center', paddingVertical: 13, borderRadius: 999, backgroundColor: picked.size === 0 ? '#1f1f1f' : pressed ? '#1a8cd8' : '#1d9bf0', })} > {sending ? ( ) : ( {picked.size === 0 ? 'Выберите контакты' : `Отправить (${picked.size})`} )} ); } // ── Row ───────────────────────────────────────────────────────────────── function ContactRow({ contact, checked, onToggle }: { contact: Contact; checked: boolean; onToggle: () => void; }) { const name = contact.username ? `@${contact.username}` : contact.alias ?? shortAddr(contact.address); return ( ({ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10, backgroundColor: pressed ? '#111111' : 'transparent', })} > {name} {shortAddr(contact.address, 8)} {/* Checkbox indicator */} {checked && } ); } function shortAddr(a: string, n = 6): string { if (!a) return '—'; return a.length <= n * 2 + 1 ? a : `${a.slice(0, n)}…${a.slice(-n)}`; } function plural(n: number): string { const mod100 = n % 100; const mod10 = n % 10; if (mod100 >= 11 && mod100 <= 19) return 'ов'; if (mod10 === 1) return ''; if (mod10 >= 2 && mod10 <= 4) return 'а'; return 'ов'; }