/** * Main app layout — кастомный `` + ``. * * AnimatedSlot — обёртка над Slot'ом, анимирующая переход при смене * pathname'а. Направление анимации вычисляется по TAB_ORDER: если * целевой tab "справа" — слайд из правой стороны, "слева" — из левой. * * Intra-tab навигация (chats/index → chats/[id]) обслуживается вложенным * Stack'ом в chats/_layout.tsx — там остаётся нативная slide-from-right * анимация, чтобы chat detail "выезжал" поверх списка. * * Side-effects (balance, contacts, WS auth, dev seed) — монтируются здесь * один раз; переходы между tab'ами их не перезапускают. */ import React, { useEffect } from 'react'; import { View } from 'react-native'; import { router, usePathname } from 'expo-router'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useStore } from '@/lib/store'; import { useBalance } from '@/hooks/useBalance'; import { useContacts } from '@/hooks/useContacts'; import { useWellKnownContracts } from '@/hooks/useWellKnownContracts'; import { useNotifications } from '@/hooks/useNotifications'; import { useGlobalInbox } from '@/hooks/useGlobalInbox'; import { getWSClient } from '@/lib/ws'; import { NavBar } from '@/components/NavBar'; import { AnimatedSlot } from '@/components/AnimatedSlot'; import { saveContact } from '@/lib/storage'; export default function AppLayout() { const keyFile = useStore(s => s.keyFile); const requests = useStore(s => s.requests); const insets = useSafeAreaInsets(); const pathname = usePathname(); // NavBar прячется на full-screen экранах: // - chat detail // - compose (new post modal) // - feed sub-routes (post detail, hashtag search) // - tx detail const hideNav = /^\/chats\/[^/]+/.test(pathname) || pathname === '/compose' || /^\/feed\/.+/.test(pathname) || /^\/tx\/.+/.test(pathname); useBalance(); useContacts(); useWellKnownContracts(); useNotifications(); // permission + tap-handler useGlobalInbox(); // global inbox listener → notifications on new peer msg // Ensure the Saved Messages (self-chat) contact exists as soon as the user // is signed in, so it shows up in the chat list without any prior action. const contacts = useStore(s => s.contacts); const upsertContact = useStore(s => s.upsertContact); useEffect(() => { if (!keyFile) return; if (contacts.some(c => c.address === keyFile.pub_key)) return; const saved = { address: keyFile.pub_key, x25519Pub: keyFile.x25519_pub, alias: 'Saved Messages', addedAt: Date.now(), }; upsertContact(saved); saveContact(saved).catch(() => { /* best-effort — re-added next boot anyway */ }); }, [keyFile, contacts, upsertContact]); useEffect(() => { const ws = getWSClient(); if (keyFile) ws.setAuthCreds({ pubKey: keyFile.pub_key, privKey: keyFile.priv_key }); else ws.setAuthCreds(null); }, [keyFile]); useEffect(() => { if (keyFile === null) { const t = setTimeout(() => { if (!useStore.getState().keyFile) router.replace('/'); }, 300); return () => clearTimeout(t); } }, [keyFile]); return ( {!hideNav && ( )} ); }