Bump to v0.6.1 — native file upload, mention names, connection stability

- Native file picker (+) in compose box uploads via Rust backend (reqwest)
- Pasting a local file path auto-uploads instead of inserting text
- @mentions resolve to profile display names via useProfile hook
- Connection indicator uses 15s grace period before showing offline
- Upload uses correct nostr.build v2 API; Rust-side multipart for native picks
- Content parser extracted to src/lib/parsing.ts
This commit is contained in:
Jure
2026-03-18 14:55:29 +01:00
parent ef189932e6
commit fbf05e8f90
16 changed files with 776 additions and 253 deletions

View File

@@ -1,17 +1,23 @@
import { create } from "zustand";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { connectToRelays, fetchGlobalFeed, getNDK } from "../lib/nostr";
import { connectToRelays, fetchGlobalFeed, fetchBatchEngagement, getNDK } from "../lib/nostr";
import { dbLoadFeed, dbSaveNotes } from "../lib/db";
const TRENDING_CACHE_KEY = "wrystr_trending_cache";
const TRENDING_TTL = 10 * 60 * 1000; // 10 minutes
interface FeedState {
notes: NDKEvent[];
loading: boolean;
connected: boolean;
error: string | null;
focusedNoteIndex: number;
trendingNotes: NDKEvent[];
trendingLoading: boolean;
connect: () => Promise<void>;
loadCachedFeed: () => Promise<void>;
loadFeed: () => Promise<void>;
loadTrendingFeed: (force?: boolean) => Promise<void>;
setFocusedNoteIndex: (n: number) => void;
}
@@ -21,6 +27,8 @@ export const useFeedStore = create<FeedState>((set, get) => ({
connected: false,
error: null,
focusedNoteIndex: -1,
trendingNotes: [],
trendingLoading: false,
setFocusedNoteIndex: (n: number) => set({ focusedNoteIndex: n }),
connect: async () => {
@@ -29,16 +37,28 @@ export const useFeedStore = create<FeedState>((set, get) => ({
await connectToRelays();
set({ connected: true });
// Monitor relay connectivity — update status if all relays disconnect
// Monitor relay connectivity with grace period.
// NDK relays can briefly show connected=false during WebSocket
// reconnection cycles, so we require multiple consecutive "offline"
// checks before flipping the indicator, and attempt reconnection.
const ndk = getNDK();
let offlineStreak = 0;
const checkConnection = () => {
const relays = Array.from(ndk.pool?.relays?.values() ?? []);
const hasConnected = relays.some((r) => r.connected);
if (get().connected !== hasConnected) {
set({ connected: hasConnected });
if (hasConnected) {
offlineStreak = 0;
if (!get().connected) set({ connected: true });
} else {
offlineStreak++;
// Only mark offline after 3 consecutive checks (15s grace)
if (offlineStreak >= 3 && get().connected) {
set({ connected: false });
// Attempt reconnection
ndk.connect().catch(() => {});
}
}
};
// Re-check periodically (relay reconnects, disconnects)
setInterval(checkConnection, 5000);
} catch (err) {
set({ error: `Connection failed: ${err}` });
@@ -79,4 +99,55 @@ export const useFeedStore = create<FeedState>((set, get) => ({
set({ error: `Feed failed: ${err}`, loading: false });
}
},
loadTrendingFeed: async (force?: boolean) => {
if (get().trendingLoading) return;
// Check cache first (skip if forced refresh)
if (!force) {
try {
const cached = localStorage.getItem(TRENDING_CACHE_KEY);
if (cached) {
const { timestamp } = JSON.parse(cached) as { noteIds: string[]; timestamp: number };
if (Date.now() - timestamp < TRENDING_TTL && get().trendingNotes.length > 0) {
return; // Cache still valid and notes already in store
}
}
} catch { /* ignore cache errors */ }
}
set({ trendingLoading: true });
try {
const notes = await fetchGlobalFeed(200);
if (notes.length === 0) {
set({ trendingNotes: [], trendingLoading: false });
return;
}
const eventIds = notes.map((n) => n.id).filter(Boolean) as string[];
const engagement = await fetchBatchEngagement(eventIds);
const scored = notes
.map((note) => {
const eng = engagement.get(note.id) ?? { reactions: 0, replies: 0, zapSats: 0 };
const score = eng.reactions * 1 + eng.replies * 3 + eng.zapSats * 0.01;
return { note, score };
})
.filter((s) => s.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 50)
.map((s) => s.note);
set({ trendingNotes: scored, trendingLoading: false });
// Cache note IDs + timestamp
localStorage.setItem(TRENDING_CACHE_KEY, JSON.stringify({
noteIds: scored.map((n) => n.id),
timestamp: Date.now(),
}));
} catch (err) {
set({ error: `Trending failed: ${err}`, trendingLoading: false });
}
},
}));