/** * Profile screen — info card about any address (yours or someone else's), * plus a Follow/Unfollow button. Posts are intentionally NOT shown here * — this screen is chat-oriented ("who is on the other side of this * conversation"); the feed tab + /feed/author/{pub} is where you go to * browse someone's timeline. * * Route: * /(app)/profile/ * * Back behaviour: * Nested Stack layout in app/(app)/profile/_layout.tsx preserves the * push stack, so tapping Back returns the user to whatever screen * pushed them here (feed card tap, chat header tap, etc.). */ import React, { useState } from 'react'; import { View, Text, ScrollView, Pressable, ActivityIndicator, } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import * as Clipboard from 'expo-clipboard'; import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useStore } from '@/lib/store'; import { Avatar } from '@/components/Avatar'; import { Header } from '@/components/Header'; import { IconButton } from '@/components/IconButton'; import { followUser, unfollowUser } from '@/lib/feed'; import { humanizeTxError } from '@/lib/api'; function shortAddr(a: string, n = 10): string { if (!a) return '—'; return a.length <= n * 2 + 1 ? a : `${a.slice(0, n)}…${a.slice(-n)}`; } export default function ProfileScreen() { const insets = useSafeAreaInsets(); const { address } = useLocalSearchParams<{ address: string }>(); const contacts = useStore(s => s.contacts); const keyFile = useStore(s => s.keyFile); const contact = contacts.find(c => c.address === address); const [following, setFollowing] = useState(false); const [followingBusy, setFollowingBusy] = useState(false); const [copied, setCopied] = useState(false); const isMe = !!keyFile && keyFile.pub_key === address; const displayName = contact?.username ? `@${contact.username}` : contact?.alias ?? (isMe ? 'Вы' : shortAddr(address ?? '', 6)); const copyAddress = async () => { if (!address) return; await Clipboard.setStringAsync(address); setCopied(true); setTimeout(() => setCopied(false), 1800); }; const openChat = () => { if (!address) return; router.replace(`/(app)/chats/${address}` as never); }; const onToggleFollow = async () => { if (!keyFile || !address || isMe || followingBusy) return; setFollowingBusy(true); const wasFollowing = following; setFollowing(!wasFollowing); try { if (wasFollowing) { await unfollowUser({ from: keyFile.pub_key, privKey: keyFile.priv_key, target: address }); } else { await followUser({ from: keyFile.pub_key, privKey: keyFile.priv_key, target: address }); } } catch (e: any) { setFollowing(wasFollowing); // Surface the error via alert — feed lib already formats humanizeTxError. alert(humanizeTxError(e)); } finally { setFollowingBusy(false); } }; return (
router.back()} />} /> {/* ── Hero: avatar + Follow button ──────────────────────────── */} {!isMe ? ( ({ paddingHorizontal: 18, paddingVertical: 9, borderRadius: 999, backgroundColor: following ? (pressed ? '#1a1a1a' : '#111111') : (pressed ? '#e7e7e7' : '#ffffff'), borderWidth: following ? 1 : 0, borderColor: '#1f1f1f', minWidth: 120, alignItems: 'center', })} > {followingBusy ? ( ) : ( {following ? 'Вы подписаны' : 'Подписаться'} )} ) : ( router.push('/(app)/settings' as never)} style={({ pressed }) => ({ paddingHorizontal: 18, paddingVertical: 9, borderRadius: 999, backgroundColor: pressed ? '#1a1a1a' : '#111111', borderWidth: 1, borderColor: '#1f1f1f', })} > Редактировать )} {/* Name + verified tick */} {displayName} {contact?.username && ( )} {/* Open chat — single CTA, full width, icon inline with text. Only when we know this is a contact (direct chat exists). */} {!isMe && contact && ( ({ marginTop: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, paddingVertical: 11, borderRadius: 999, backgroundColor: pressed ? '#1a1a1a' : '#111111', borderWidth: 1, borderColor: '#1f1f1f', })} > Открыть чат )} {/* ── Info card ───────────────────────────────────────────────── */} {/* Address — entire row is tappable → copies */} ({ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12, backgroundColor: pressed ? '#0f0f0f' : 'transparent', })} > Адрес {copied ? 'Скопировано' : shortAddr(address ?? '')} {/* Encryption status */} {contact && ( <> {/* Group-only: participant count. DMs always have exactly two people so the row would be noise. Groups would show real member count here from chain state once v2.1.0 ships groups. */} {contact.kind === 'group' && ( <> )} )} {!contact && !isMe && ( Этот пользователь пока не в ваших контактах. Нажмите «Подписаться», чтобы видеть его посты в ленте, или добавьте в чаты через @username. )} ); } function Divider() { return ; } function InfoRow({ label, value, icon, danger, }: { label: string; value: string; icon?: React.ComponentProps['name']; danger?: boolean; }) { return ( {icon && ( )} {label} {value} ); }