DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
128 lines
3.8 KiB
TypeScript
128 lines
3.8 KiB
TypeScript
/**
|
|
* Main app tab layout.
|
|
* Redirects to welcome if no key found.
|
|
*/
|
|
|
|
import React, { useEffect } from 'react';
|
|
import { Tabs, router } from 'expo-router';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
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 { getWSClient } from '@/lib/ws';
|
|
|
|
const C_ACCENT = '#7db5ff';
|
|
const C_MUTED = '#98a7c2';
|
|
const C_BG = '#111a2b';
|
|
const C_BORDER = '#1c2840';
|
|
|
|
export default function AppLayout() {
|
|
const keyFile = useStore(s => s.keyFile);
|
|
const requests = useStore(s => s.requests);
|
|
const insets = useSafeAreaInsets();
|
|
|
|
useBalance();
|
|
useContacts();
|
|
useWellKnownContracts(); // auto-discover canonical system contracts from node
|
|
|
|
// Arm the WS client with this user's Ed25519 keypair. The client signs the
|
|
// server's auth nonce on every (re)connect so scoped subscriptions
|
|
// (addr:<my_pub>, inbox:<my_x25519>) are accepted. Without this the
|
|
// server would still accept global topic subs but reject scoped ones.
|
|
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]);
|
|
|
|
// Tab bar layout math:
|
|
// icon (22) + gap (4) + label (~13) = ~39px of content
|
|
// We add a 12px visual margin above, and pad the bottom by the larger of
|
|
// the platform safe-area inset or 10px so the bar never sits flush on the
|
|
// home indicator.
|
|
const BAR_CONTENT_HEIGHT = 52;
|
|
const bottomPad = Math.max(insets.bottom, 10);
|
|
|
|
return (
|
|
<Tabs
|
|
screenOptions={{
|
|
headerShown: false,
|
|
tabBarActiveTintColor: C_ACCENT,
|
|
tabBarInactiveTintColor: C_MUTED,
|
|
tabBarLabelStyle: {
|
|
fontSize: 10,
|
|
fontWeight: '500',
|
|
marginTop: 2,
|
|
},
|
|
tabBarStyle: {
|
|
backgroundColor: C_BG,
|
|
borderTopColor: C_BORDER,
|
|
borderTopWidth: 1,
|
|
height: BAR_CONTENT_HEIGHT + bottomPad,
|
|
paddingTop: 8,
|
|
paddingBottom: bottomPad,
|
|
},
|
|
}}
|
|
>
|
|
<Tabs.Screen
|
|
name="chats"
|
|
options={{
|
|
tabBarLabel: 'Чаты',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons
|
|
name={focused ? 'chatbubbles' : 'chatbubbles-outline'}
|
|
size={22}
|
|
color={color}
|
|
/>
|
|
),
|
|
tabBarBadge: requests.length > 0 ? requests.length : undefined,
|
|
tabBarBadgeStyle: { backgroundColor: C_ACCENT, fontSize: 10 },
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="wallet"
|
|
options={{
|
|
tabBarLabel: 'Кошелёк',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons
|
|
name={focused ? 'wallet' : 'wallet-outline'}
|
|
size={22}
|
|
color={color}
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="settings"
|
|
options={{
|
|
tabBarLabel: 'Настройки',
|
|
tabBarIcon: ({ color, focused }) => (
|
|
<Ionicons
|
|
name={focused ? 'settings' : 'settings-outline'}
|
|
size={22}
|
|
color={color}
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
{/* Non-tab screens — hidden from tab bar */}
|
|
<Tabs.Screen name="requests" options={{ href: null }} />
|
|
<Tabs.Screen name="new-contact" options={{ href: null }} />
|
|
</Tabs>
|
|
);
|
|
}
|