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;
}