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