Files
dchain/client-app/app/(app)/chats/index.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

114 lines
4.4 KiB
TypeScript
Raw Permalink 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.

/**
* Messages screen — список чатов в стиле референса.
*
* ┌ safe-area top
* │ TabHeader (title зависит от connection state)
* │ ─ FlatList (chat tiles) ─
* └ NavBar (external)
*
* Фильтры и search убраны — лист один поток; requests доступны через
* NavBar → notifications tab. FAB composer'а тоже убран (чат-лист
* просто отражает существующие беседы, создание новых — через tab
* "New chat" в NavBar'е).
*/
import React, { useMemo } from 'react';
import { View, Text, FlatList } from 'react-native';
import { router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useStore } from '@/lib/store';
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
import type { Contact, Message } from '@/lib/types';
import { TabHeader } from '@/components/TabHeader';
import { ChatTile } from '@/components/ChatTile';
export default function ChatsScreen() {
const insets = useSafeAreaInsets();
const contacts = useStore(s => s.contacts);
const messages = useStore(s => s.messages);
const keyFile = useStore(s => s.keyFile);
// Статус подключения: online / connecting / offline.
// Название шапки и цвет pip'а на аватаре зависят от него.
const connStatus = useConnectionStatus();
const headerTitle =
connStatus === 'online' ? 'Messages' :
connStatus === 'connecting' ? 'Connecting…' :
'Waiting for internet';
const dotColor =
connStatus === 'online' ? '#3ba55d' : // green
connStatus === 'connecting' ? '#f0b35a' : // amber
'#f4212e'; // red
const lastOf = (c: Contact): Message | null => {
const msgs = messages[c.address];
return msgs && msgs.length ? msgs[msgs.length - 1] : null;
};
// Сортировка по последней активности. Saved Messages (self-chat) всегда
// закреплён сверху — это "Избранное", бессмысленно конкурировать с ним
// по recency'и обычным чатам.
const selfAddr = keyFile?.pub_key;
const sorted = useMemo(() => {
const saved = selfAddr ? contacts.find(c => c.address === selfAddr) : undefined;
const rest = contacts
.filter(c => c.address !== selfAddr)
.map(c => ({ c, last: lastOf(c) }))
.sort((a, b) => {
const ka = a.last ? a.last.timestamp : a.c.addedAt / 1000;
const kb = b.last ? b.last.timestamp : b.c.addedAt / 1000;
return kb - ka;
})
.map(x => x.c);
return saved ? [saved, ...rest] : rest;
}, [contacts, messages, selfAddr]);
return (
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<TabHeader title={headerTitle} profileDotColor={dotColor} />
<View style={{ flex: 1 }}>
<FlatList
data={sorted}
keyExtractor={c => c.address}
renderItem={({ item }) => (
<ChatTile
contact={item}
lastMessage={lastOf(item)}
saved={item.address === selfAddr}
onPress={() => router.push(`/(app)/chats/${item.address}` as never)}
/>
)}
contentContainerStyle={{ paddingBottom: 40, flexGrow: 1 }}
showsVerticalScrollIndicator={false}
/>
{sorted.length === 0 && (
<View
pointerEvents="box-none"
style={{
position: 'absolute',
left: 0, right: 0, top: 0, bottom: 0,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
}}
>
<Ionicons name="chatbubbles-outline" size={42} color="#3a3a3a" style={{ marginBottom: 10 }} />
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginBottom: 6 }}>
No chats yet
</Text>
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', lineHeight: 20 }}>
Use the search tab in the navbar to add your first contact.
</Text>
</View>
)}
</View>
</View>
);
}