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:
vsecoder
2026-04-19 13:14:47 +03:00
parent e6f3d2bcf8
commit a75cbcd224
18 changed files with 870 additions and 102 deletions

View File

@@ -6,6 +6,7 @@
*/
import React from 'react';
import { View, Text } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
export interface AvatarProps {
/** Имя / @username — берём первый символ для placeholder. */
@@ -18,6 +19,11 @@ export interface AvatarProps {
dotColor?: string;
/** Класс для обёртки (position: relative кадр). */
className?: string;
/**
* Saved Messages variant — blue circle with a bookmark glyph, Telegram-style.
* When set, `name`/`address` are ignored for the visual.
*/
saved?: boolean;
}
/** Простое хэширование имени → один из 6 оттенков серого для разнообразия. */
@@ -28,10 +34,10 @@ function pickBg(seed: string): string {
return shades[h % shades.length];
}
export function Avatar({ name, address, size = 48, dotColor, className }: AvatarProps) {
export function Avatar({ name, address, size = 48, dotColor, className, saved }: AvatarProps) {
const seed = (name ?? address ?? '?').replace(/^@/, '');
const initial = seed.charAt(0).toUpperCase() || '?';
const bg = pickBg(seed);
const bg = saved ? '#1d9bf0' : pickBg(seed);
return (
<View className={className} style={{ width: size, height: size, position: 'relative' }}>
@@ -45,16 +51,20 @@ export function Avatar({ name, address, size = 48, dotColor, className }: Avatar
justifyContent: 'center',
}}
>
<Text
style={{
color: '#d0d0d0',
fontSize: size * 0.4,
fontWeight: '600',
includeFontPadding: false,
}}
>
{initial}
</Text>
{saved ? (
<Ionicons name="bookmark" size={size * 0.5} color="#ffffff" />
) : (
<Text
style={{
color: '#d0d0d0',
fontSize: size * 0.4,
fontWeight: '600',
includeFontPadding: false,
}}
>
{initial}
</Text>
)}
</View>
{dotColor && (
<View

View File

@@ -57,10 +57,12 @@ export interface ChatTileProps {
contact: Contact;
lastMessage: Message | null;
onPress: () => void;
/** Render as the Saved Messages tile (blue bookmark avatar, fixed name). */
saved?: boolean;
}
export function ChatTile({ contact: c, lastMessage, onPress }: ChatTileProps) {
const name = displayName(c);
export function ChatTile({ contact: c, lastMessage, onPress, saved }: ChatTileProps) {
const name = saved ? 'Saved Messages' : displayName(c);
const last = lastMessage;
// Визуальный маркер типа чата.
@@ -92,7 +94,8 @@ export function ChatTile({ contact: c, lastMessage, onPress }: ChatTileProps) {
name={name}
address={c.address}
size={44}
dotColor={c.x25519Pub && (!c.kind || c.kind === 'direct') ? '#3ba55d' : undefined}
saved={saved}
dotColor={!saved && c.x25519Pub && (!c.kind || c.kind === 'direct') ? '#3ba55d' : undefined}
/>
<View style={{ flex: 1, marginLeft: 12, minWidth: 0 }}>
@@ -143,9 +146,11 @@ export function ChatTile({ contact: c, lastMessage, onPress }: ChatTileProps) {
>
{last
? lastPreview(last)
: c.x25519Pub
? 'Tap to start encrypted chat'
: 'Waiting for identity…'}
: saved
? 'Your personal notes & files'
: c.x25519Pub
? 'Tap to start encrypted chat'
: 'Waiting for identity…'}
</Text>
{unread !== null && (

View File

@@ -80,11 +80,16 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }:
// Find a display-friendly name for the author. If it's a known contact
// with @username, use that; otherwise short-addr.
//
// `mine` takes precedence over the contact lookup: our own pub key has
// a self-contact entry with alias "Saved Messages" (that's how the
// self-chat tile is rendered), but that label is wrong in the feed —
// posts there should read as "You", not as a messaging-app affordance.
const displayName = useMemo(() => {
if (mine) return 'You';
const c = contacts.find(x => x.address === post.author);
if (c?.username) return `@${c.username}`;
if (c?.alias) return c.alias;
if (mine) return 'You';
return shortAddr(post.author);
}, [contacts, post.author, mine]);