Files
dchain/client-app/app/(app)/_layout.tsx
vsecoder a75cbcd224 feat: resource caps, Saved Messages, author walls, docs for node bring-up
Node flags (cmd/node/main.go):
  --max-cpu / --max-ram-mb — Go runtime caps (GOMAXPROCS / GOMEMLIMIT)
  --feed-disk-limit-mb — hard 507 refusal for new post bodies over quota
  --chain-disk-limit-mb — advisory watcher (can't reject blocks without
  breaking consensus; logs WARN every minute)

Client — Saved Messages (self-chat):
  - Auto-created on sign-in, pinned top of chat list, blue bookmark avatar
  - Send short-circuits the relay (no encrypt, no fee, no mailbox hop)
  - Empty state rendered outside inverted FlatList — fixes the mirrored
    "say hi…" on Android RTL-aware layout builds
  - PostCard shows "You" for own posts instead of the self-contact alias

Client — user walls:
  - New route /(app)/feed/author/[pub] with infinite-scroll via
    `created_at` cursor and pull-to-refresh
  - Profile screen gains "View posts" button (universal) next to
    "Open chat" (contact-only)

Feed pipeline:
  - Bump client JPEG quality 0.5 → 0.75 to match server scrubber (Q=75),
    so a 60 KiB compose doesn't balloon past 256 KiB after server re-encode
  - ErrPostTooLarge now wraps with the actual size vs cap, errors.Is
    preserved in the HTTP layer
  - FeedMailbox quota + DiskUsage surface — supports new CLI flag

README:
  - Step-by-step "first node / joiner" section on the landing page,
    full flag tables incl. the new resource-cap group, minimal
    checklists for open/private/low-end deployments
2026-04-19 13:14:47 +03:00

100 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Main app layout — кастомный `<AnimatedSlot>` + `<NavBar>`.
*
* 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 (
<View style={{ flex: 1, backgroundColor: '#000000' }}>
<View style={{ flex: 1 }}>
<AnimatedSlot />
</View>
{!hideNav && (
<NavBar
bottomInset={insets.bottom}
requestCount={requests.length}
notifCount={0}
/>
)}
</View>
);
}