/** * Contact requests / notifications — dark minimalist. * * В референсе нижний таб «notifications» ведёт сюда. Пока это только * incoming CONTACT_REQUEST'ы; позже сюда же придут другие системные * уведомления (slash, ADD_VALIDATOR со-sig-ing, и т.д.). */ import React, { useState } from 'react'; import { View, Text, FlatList, Alert, Pressable, ActivityIndicator } from 'react-native'; import { router } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useStore } from '@/lib/store'; import { buildAcceptContactTx, submitTx, getIdentity, humanizeTxError, } from '@/lib/api'; import { saveContact } from '@/lib/storage'; import { shortAddr } from '@/lib/crypto'; import { relativeTime } from '@/lib/utils'; import type { ContactRequest } from '@/lib/types'; import { Avatar } from '@/components/Avatar'; import { TabHeader } from '@/components/TabHeader'; import { IconButton } from '@/components/IconButton'; export default function RequestsScreen() { const insets = useSafeAreaInsets(); const keyFile = useStore(s => s.keyFile); const requests = useStore(s => s.requests); const setRequests = useStore(s => s.setRequests); const upsertContact = useStore(s => s.upsertContact); const [accepting, setAccepting] = useState(null); async function accept(req: ContactRequest) { if (!keyFile) return; setAccepting(req.txHash); try { const identity = await getIdentity(req.from); const x25519Pub = identity?.x25519_pub ?? ''; const tx = buildAcceptContactTx({ from: keyFile.pub_key, to: req.from, privKey: keyFile.priv_key, }); await submitTx(tx); const contact = { address: req.from, x25519Pub, username: req.username, addedAt: Date.now() }; upsertContact(contact); await saveContact(contact); setRequests(requests.filter(r => r.txHash !== req.txHash)); router.replace(`/(app)/chats/${req.from}` as never); } catch (e: any) { Alert.alert('Accept failed', humanizeTxError(e)); } finally { setAccepting(null); } } function decline(req: ContactRequest) { Alert.alert( 'Decline request', `Decline request from ${req.username ? '@' + req.username : shortAddr(req.from)}?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Decline', style: 'destructive', onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)), }, ], ); } const renderItem = ({ item: req }: { item: ContactRequest }) => { const name = req.username ? `@${req.username}` : shortAddr(req.from); const isAccepting = accepting === req.txHash; return ( {name} wants to message you · {relativeTime(req.timestamp)} {req.intro ? ( {req.intro} ) : null} accept(req)} disabled={isAccepting} style={({ pressed }) => ({ flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 9, borderRadius: 999, backgroundColor: isAccepting ? '#1a1a1a' : pressed ? '#1a8cd8' : '#1d9bf0', })} > {isAccepting ? ( ) : ( Accept )} decline(req)} disabled={isAccepting} style={({ pressed }) => ({ flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 9, borderRadius: 999, backgroundColor: pressed ? '#1a1a1a' : '#111111', borderWidth: 1, borderColor: '#1f1f1f', })} > Decline ); }; return ( {requests.length === 0 ? ( All caught up Contact requests and network events will appear here. ) : ( r.txHash} renderItem={renderItem} contentContainerStyle={{ paddingBottom: 120 }} showsVerticalScrollIndicator={false} /> )} ); }