diff --git a/client-app/app/(app)/feed/index.tsx b/client-app/app/(app)/feed/index.tsx index 87e4c9d..2dc8c97 100644 --- a/client-app/app/(app)/feed/index.tsx +++ b/client-app/app/(app)/feed/index.tsx @@ -97,9 +97,11 @@ export default function FeedScreen() { } catch (e: any) { if (seq !== requestRef.current) return; const msg = String(e?.message ?? e); - // Silence benign network/404 — just show empty state. + // Network / 404 is benign — node just unreachable or empty. In __DEV__ + // fall back to synthetic seed posts so the scroll / tap UI stays + // testable; in production this path shows the empty state. if (/Network request failed|→\s*404/.test(msg)) { - setPosts([]); + setPosts(getDevSeedFeed()); } else { setError(msg); } diff --git a/client-app/app/(app)/profile/[address].tsx b/client-app/app/(app)/profile/[address].tsx index b727d76..35e8391 100644 --- a/client-app/app/(app)/profile/[address].tsx +++ b/client-app/app/(app)/profile/[address].tsx @@ -244,14 +244,19 @@ export default function ProfileScreen() { icon="calendar-outline" /> - {/* Participants count — 1 for direct DMs. Groups would show - their actual member count from chain state (v2.1.0+). */} - - + {/* 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' && ( + <> + + + + )} )} diff --git a/client-app/components/AnimatedSlot.tsx b/client-app/components/AnimatedSlot.tsx index 6673fd8..008e9ed 100644 --- a/client-app/components/AnimatedSlot.tsx +++ b/client-app/components/AnimatedSlot.tsx @@ -1,67 +1,35 @@ /** - * AnimatedSlot — обёртка над ``. Исторически тут была slide- - * анимация при смене pathname'а + tab-swipe pan. Обе фичи вызывали - * баги: - * - tab-swipe конфликтовал с vertical FlatList scroll (чаты пропадали - * при flick'е) - * - translateX застревал на ±width когда анимация прерывалась - * re-render-cascade'ом от useGlobalInbox → UI уезжал за экран + * AnimatedSlot — renders the (app) group as a native . * - * Решение: убрали обе. Навигация между tab'ами — только через NavBar, - * переходы — без slide. Sub-route back-swipe остаётся (он не конфликтует - * с FlatList'ом, т.к. на chat detail FlatList inverted и смотрит вверх). + * Why Stack instead of Slot: Slot is stack-less. When a child route + * (e.g. profile/[address]) pushes another child, Slot swaps content + * with no history, so router.back() falls all the way through to the + * URL root instead of returning to the caller (e.g. /chats/xyz → + * /profile/abc → back should go to /chats/xyz, not to /chats). + * + * Tab switching stays flat because NavBar uses router.replace, which + * maps to navigation.replace on the Stack → no history accumulation. + * + * animation: 'none' on tab roots keeps tab-swap instant (matches the + * prior Slot look). Sub-routes (profile/*, compose, feed/*, chats/[id]) + * inherit slide_from_right from their own nested _layout.tsx Stacks, + * which is where the push animation happens. + * + * This file is named AnimatedSlot for git-history continuity — the + * Animated.Value + translateX slide was removed earlier (got stuck at + * ±width when interrupted by re-render cascades). */ -import React, { useMemo } from 'react'; -import { PanResponder, View } from 'react-native'; -import { Slot, usePathname, router } from 'expo-router'; -import { useWindowDimensions } from 'react-native'; - -function topSegment(p: string): string { - const m = p.match(/^\/[^/]+/); - return m ? m[0] : ''; -} - -/** Это sub-route — внутри какого-либо tab'а, но глубже первого сегмента. */ -function isSubRoute(path: string): boolean { - const seg = topSegment(path); - if (seg === '/chats') return path !== '/chats' && path !== '/chats/'; - if (seg === '/feed') return path !== '/feed' && path !== '/feed/'; - if (seg === '/profile') return true; - if (seg === '/settings') return true; - if (seg === '/compose') return true; - return false; -} +import React from 'react'; +import { Stack } from 'expo-router'; export function AnimatedSlot() { - const pathname = usePathname(); - const { width } = useWindowDimensions(); - - // Pan-gesture только на sub-route'ах: swipe-right → back. На tab'ах - // gesture полностью отключён — исключает конфликт с vertical scroll. - const panResponder = useMemo(() => { - const sub = isSubRoute(pathname); - - return PanResponder.create({ - onMoveShouldSetPanResponder: (_e, g) => { - if (!sub) return false; - if (Math.abs(g.dx) < 40) return false; - if (Math.abs(g.dy) > 15) return false; - if (g.dx <= 0) return false; // только правое направление - return Math.abs(g.dx) > Math.abs(g.dy) * 3; - }, - onMoveShouldSetPanResponderCapture: () => false, - - onPanResponderRelease: (_e, g) => { - if (!sub) return; - if (Math.abs(g.dy) > 30) return; - if (g.dx > width * 0.30) router.back(); - }, - }); - }, [pathname, width]); - return ( - - - + ); } diff --git a/client-app/lib/devSeedFeed.ts b/client-app/lib/devSeedFeed.ts index aa61517..b46a875 100644 --- a/client-app/lib/devSeedFeed.ts +++ b/client-app/lib/devSeedFeed.ts @@ -164,18 +164,17 @@ const SEED_POSTS: FeedPostItem[] = [ }, ]; -/** True when the current build is a Metro dev bundle. __DEV__ is a - * global injected by Metro at bundle time and typed via react-native's - * ambient declarations, so no ts-ignore is needed. */ -function isDev(): boolean { - return typeof __DEV__ !== 'undefined' && __DEV__ === true; -} - /** - * Returns the dev-seed post list (only in __DEV__). Called by the Feed - * screen as a fallback when the real API returned an empty list. + * Returns the dev-seed post list. Only returns actual items in dev + * builds; release bundles return an empty array so fake posts never + * appear in production. + * + * We use the runtime `globalThis.__DEV__` lookup rather than the typed + * `__DEV__` global because some builds can have the TS typing + * out-of-sync with the actual injected value. */ export function getDevSeedFeed(): FeedPostItem[] { - if (!isDev()) return []; + const g = globalThis as unknown as { __DEV__?: boolean }; + if (g.__DEV__ !== true) return []; return SEED_POSTS; }