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
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
}
|