Nostr transport: - Switch from kind 1 to NIP-78 kind 30078 (application-specific data) - Relays store events persistently — no inventory protocol needed - AES-256-GCM encryption via Web Crypto API (room key = shared secret) - PBKDF2 key derivation (100k iterations) from room key - Chunking for large events (images >48KB) with per-chunk encryption - Auto re-publish missing local events on room join - Manual "Re-sync" button for recovery of failed publishes Performance (all variants): - Emit 'change' immediately after local store write (IDB/NeDB/SQLite) - Folder and Nostr writes run fire-and-forget in background - Fast event hashing: fingerprint metadata only, skip full payload SHA-256 - Saving spinner in paste UI while write completes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { FolderSyncDB } from '../../nostr/src/index.ts';
|
|
import { initPasteApp } from '../shared.ts';
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
const db = await FolderSyncDB.open({ autoSyncIntervalMs: 3000 });
|
|
await initPasteApp(db as any, 'nostr');
|
|
|
|
// ── Nostr room UI ──────────────────────────────────────────
|
|
|
|
const roomInput = document.querySelector('#room-input') as HTMLInputElement;
|
|
const joinBtn = document.querySelector('#join-room') as HTMLButtonElement;
|
|
const nostrStatus = document.querySelector('#nostr-status')!;
|
|
|
|
function updateNostrUI(connected: boolean, room?: string) {
|
|
if (connected && room) {
|
|
nostrStatus.textContent = `Connected: ${room}`;
|
|
nostrStatus.className = 'status ok';
|
|
joinBtn.textContent = 'Leave';
|
|
roomInput.disabled = true;
|
|
} else {
|
|
nostrStatus.textContent = 'Not connected';
|
|
nostrStatus.className = 'status';
|
|
joinBtn.textContent = 'Join room';
|
|
roomInput.disabled = false;
|
|
}
|
|
}
|
|
|
|
joinBtn.addEventListener('click', async () => {
|
|
if (db.isConnected()) {
|
|
db.leaveRoom();
|
|
updateNostrUI(false);
|
|
} else {
|
|
const key = roomInput.value.trim();
|
|
if (!key) {
|
|
nostrStatus.textContent = 'Enter a room key';
|
|
nostrStatus.className = 'status err';
|
|
return;
|
|
}
|
|
try {
|
|
nostrStatus.textContent = 'Connecting...';
|
|
nostrStatus.className = 'status';
|
|
await db.joinRoom(key);
|
|
updateNostrUI(true, key);
|
|
} catch (e: unknown) {
|
|
nostrStatus.textContent = 'Error: ' + (e as Error).message;
|
|
nostrStatus.className = 'status err';
|
|
}
|
|
}
|
|
});
|
|
|
|
roomInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter') joinBtn.click();
|
|
});
|
|
|
|
// ── Re-sync button ───────────────────────────────────────
|
|
const resyncBtn = document.querySelector('#resync-btn') as HTMLButtonElement;
|
|
resyncBtn.addEventListener('click', async () => {
|
|
if (!db.isConnected()) {
|
|
nostrStatus.textContent = 'Join a room first';
|
|
nostrStatus.className = 'status err';
|
|
return;
|
|
}
|
|
resyncBtn.disabled = true;
|
|
resyncBtn.textContent = 'Syncing...';
|
|
try {
|
|
await db.republishMissing();
|
|
await db.sync();
|
|
resyncBtn.textContent = 'Done!';
|
|
setTimeout(() => { resyncBtn.textContent = 'Re-sync'; resyncBtn.disabled = false; }, 2000);
|
|
} catch (e: unknown) {
|
|
resyncBtn.textContent = 'Re-sync';
|
|
resyncBtn.disabled = false;
|
|
nostrStatus.textContent = 'Re-sync error: ' + (e as Error).message;
|
|
nostrStatus.className = 'status err';
|
|
}
|
|
});
|
|
|
|
// Restore saved room
|
|
if (db.isConnected()) {
|
|
updateNostrUI(true, db.currentRoom ?? undefined);
|
|
roomInput.value = db.currentRoom ?? '';
|
|
} else {
|
|
updateNostrUI(false);
|
|
}
|
|
|
|
db.on('nostr:connected', (data: any) => {
|
|
updateNostrUI(true, data?.roomKey);
|
|
});
|
|
|
|
db.on('nostr:disconnected', () => {
|
|
updateNostrUI(false);
|
|
});
|
|
});
|