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
This commit is contained in:
@@ -63,6 +63,24 @@ export default function ChatScreen() {
|
||||
clearContactNotifications(contactAddress);
|
||||
}, [contactAddress, clearUnread]);
|
||||
|
||||
const upsertContact = useStore(s => s.upsertContact);
|
||||
const isSavedMessages = !!keyFile && contactAddress === keyFile.pub_key;
|
||||
|
||||
// Auto-materialise the Saved Messages contact the first time the user
|
||||
// opens chat-with-self. The contact is stored locally only — no on-chain
|
||||
// CONTACT_REQUEST needed, since both ends are the same key pair.
|
||||
useEffect(() => {
|
||||
if (!isSavedMessages || !keyFile) return;
|
||||
const existing = contacts.find(c => c.address === keyFile.pub_key);
|
||||
if (existing) return;
|
||||
upsertContact({
|
||||
address: keyFile.pub_key,
|
||||
x25519Pub: keyFile.x25519_pub,
|
||||
alias: 'Saved Messages',
|
||||
addedAt: Date.now(),
|
||||
});
|
||||
}, [isSavedMessages, keyFile, contacts, upsertContact]);
|
||||
|
||||
const contact = contacts.find(c => c.address === contactAddress);
|
||||
const chatMsgs = messages[contactAddress ?? ''] ?? [];
|
||||
const listRef = useRef<FlatList>(null);
|
||||
@@ -137,9 +155,11 @@ export default function ChatScreen() {
|
||||
});
|
||||
}, [contactAddress, setMsgs]);
|
||||
|
||||
const name = contact?.username
|
||||
? `@${contact.username}`
|
||||
: contact?.alias ?? shortAddr(contactAddress ?? '');
|
||||
const name = isSavedMessages
|
||||
? 'Saved Messages'
|
||||
: contact?.username
|
||||
? `@${contact.username}`
|
||||
: contact?.alias ?? shortAddr(contactAddress ?? '');
|
||||
|
||||
// ── Compose actions ────────────────────────────────────────────────────
|
||||
const cancelCompose = useCallback(() => {
|
||||
@@ -172,7 +192,7 @@ export default function ChatScreen() {
|
||||
const hasText = !!actualText.trim();
|
||||
const hasAttach = !!actualAttach;
|
||||
if (!hasText && !hasAttach) return;
|
||||
if (!contact.x25519Pub) {
|
||||
if (!isSavedMessages && !contact.x25519Pub) {
|
||||
Alert.alert('No encryption key yet', 'The contact has not published their key. Try later.');
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +208,10 @@ export default function ChatScreen() {
|
||||
|
||||
setSending(true);
|
||||
try {
|
||||
if (hasText) {
|
||||
// Saved Messages short-circuits the relay entirely — the message never
|
||||
// leaves the device, so no encryption/fee/network round-trip is needed.
|
||||
// Regular chats still go through the NaCl + relay pipeline below.
|
||||
if (hasText && !isSavedMessages) {
|
||||
const { nonce, ciphertext } = encryptMessage(
|
||||
actualText.trim(), keyFile.x25519_priv, contact.x25519Pub,
|
||||
);
|
||||
@@ -224,7 +247,7 @@ export default function ChatScreen() {
|
||||
setSending(false);
|
||||
}
|
||||
}, [
|
||||
text, keyFile, contact, composeMode, chatMsgs,
|
||||
text, keyFile, contact, composeMode, chatMsgs, isSavedMessages,
|
||||
setMsgs, cancelCompose, appendMsg, pendingAttach,
|
||||
]);
|
||||
|
||||
@@ -411,7 +434,7 @@ export default function ChatScreen() {
|
||||
hitSlop={4}
|
||||
style={{ flexDirection: 'row', alignItems: 'center', gap: 8, maxWidth: '100%' }}
|
||||
>
|
||||
<Avatar name={name} address={contactAddress} size={28} />
|
||||
<Avatar name={name} address={contactAddress} size={28} saved={isSavedMessages} />
|
||||
<View style={{ minWidth: 0, flexShrink: 1 }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
@@ -429,7 +452,7 @@ export default function ChatScreen() {
|
||||
typing…
|
||||
</Text>
|
||||
)}
|
||||
{!peerTyping && !contact?.x25519Pub && (
|
||||
{!peerTyping && !isSavedMessages && !contact?.x25519Pub && (
|
||||
<Text style={{ color: '#f0b35a', fontSize: 11 }}>
|
||||
waiting for key
|
||||
</Text>
|
||||
@@ -447,37 +470,49 @@ export default function ChatScreen() {
|
||||
с "scroll position at bottom" без ручного scrollToEnd, и новые
|
||||
сообщения (добавляемые в начало reversed-массива) появляются
|
||||
внизу естественно. Никаких jerk'ов при открытии. */}
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={rows}
|
||||
inverted
|
||||
keyExtractor={r => r.kind === 'sep' ? r.id : r.msg.id}
|
||||
renderItem={renderRow}
|
||||
contentContainerStyle={{ paddingVertical: 10 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
// Lazy render: only mount ~1.5 screens of bubbles initially,
|
||||
// render further batches as the user scrolls older. Keeps
|
||||
// initial paint fast on chats with thousands of messages.
|
||||
initialNumToRender={25}
|
||||
maxToRenderPerBatch={12}
|
||||
windowSize={10}
|
||||
removeClippedSubviews
|
||||
ListEmptyComponent={() => (
|
||||
<View style={{
|
||||
flex: 1, alignItems: 'center', justifyContent: 'center',
|
||||
paddingHorizontal: 32, gap: 10,
|
||||
transform: [{ scaleY: -1 }], // inverted flips cells; un-flip empty state
|
||||
}}>
|
||||
<Avatar name={name} address={contactAddress} size={72} />
|
||||
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginTop: 10 }}>
|
||||
Say hi to {name}
|
||||
</Text>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', lineHeight: 20 }}>
|
||||
Your messages are end-to-end encrypted.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
{rows.length === 0 ? (
|
||||
// Empty state is rendered as a plain View instead of
|
||||
// ListEmptyComponent on an inverted FlatList — the previous
|
||||
// `transform: [{ scaleY: -1 }]` un-flip trick was rendering
|
||||
// text mirrored on some Android builds (RTL-aware layout),
|
||||
// giving us the "say hi…" backwards bug.
|
||||
<View style={{
|
||||
flex: 1, alignItems: 'center', justifyContent: 'center',
|
||||
paddingHorizontal: 32, gap: 10,
|
||||
}}>
|
||||
<Avatar
|
||||
name={name}
|
||||
address={contactAddress}
|
||||
size={72}
|
||||
saved={isSavedMessages}
|
||||
/>
|
||||
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginTop: 10 }}>
|
||||
{isSavedMessages ? 'Notes to self' : `Say hi to ${name}`}
|
||||
</Text>
|
||||
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', lineHeight: 20 }}>
|
||||
{isSavedMessages
|
||||
? 'Anything you send here stays on your device — use it as a scratchpad for links, drafts, or files.'
|
||||
: 'Your messages are end-to-end encrypted.'}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={rows}
|
||||
inverted
|
||||
keyExtractor={r => r.kind === 'sep' ? r.id : r.msg.id}
|
||||
renderItem={renderRow}
|
||||
contentContainerStyle={{ paddingVertical: 10 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
// Lazy render: only mount ~1.5 screens of bubbles initially,
|
||||
// render further batches as the user scrolls older. Keeps
|
||||
// initial paint fast on chats with thousands of messages.
|
||||
initialNumToRender={25}
|
||||
maxToRenderPerBatch={12}
|
||||
windowSize={10}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Composer — floating, прибит к низу. */}
|
||||
<View style={{ paddingBottom: Math.max(insets.bottom, 4) + 6, backgroundColor: '#000000' }}>
|
||||
|
||||
Reference in New Issue
Block a user