PR #4 of the multi-device roadmap — desktop client groundwork. The shell compiles and runs end-to-end on top of a v2.2.0 node; sections are placeholders that later alphas fill in with real chat / feed / wallet / contacts / settings content shared with the mobile client-app. Scaffold: * Vite + React + TypeScript renderer; Electron main/preload TS compiled via a separate tsconfig. * npm scripts — `dev` (concurrent Vite + Electron), `build` (installer via electron-builder), `typecheck`. * electron-builder targets: .dmg / .exe / .AppImage + .deb. * CSP pins script-src 'self'; connect-src left open so the renderer can hit any configured node. Electron main + preload: * Frame-less window, hiddenInset on macOS, custom-overlay on Windows, drag region via CSS -webkit-app-region: drag on our TitleBar. * contextIsolation on, nodeIntegration off, sandbox off (needed for safeStorage in preload). * window.dchain.keyfile.{load,save,delete,encryptionAvailable} — keyfile lives in the OS keychain via Electron safeStorage, with a plaintext fallback for OSes without an encryption backend. * window.dchain.dialog.{openFile,saveFile}, .fs.{readText,writeText}, .app.{version,platform}. Everything else still goes over plain fetch() in the renderer. Shell (src/shell/): * TitleBar — draggable 32px strip; DChain brand. * NavBar — left 72px rail, six sections + Cmd+1..5 keybinds. * StatusBar — ● online/connecting/offline dot, node URL, current chain height (polls /api/netstats every 5s). * Shell — composes the 3 panes; picks { List, Detail } by active section. Sections (all stubs — filling in alpha5+): * Messages, Feed, Contacts, Profile — SectionPlaceholder with notes. * Wallet — shows the balance reading from /api/address/{pub} as a first real data binding. * Settings — node-URL card with live ping + commit, identity card (shows pub key), about card (reads Electron app.version via IPC). Auth (src/auth/Welcome.tsx): * Create — generates Ed25519 + X25519 via tweetnacl, saves via IPC. * Import — Electron dialog.openFile → parses node.json → saves. * Pair — stub routed; real poll loop reuses the mobile flow in alpha5. Lib (src/lib/): * types.ts — KeyFile / Contact / Message / NodeSettings mirroring client-app wire formats. * storage.ts — keyfile via IPC, settings + contacts + device-registered marker via localStorage. * api.ts — fetch wrapper with setNodeUrl + onNodeUrlChange; getNetStats, getIdentity, fetchDevices, getBalance bindings. * store.ts — zustand { booted, keyFile, settings, contacts, section }. docs/ROADMAP.md — desktop subsection updated with per-alpha breakdown. Next (alpha5): Messages section wired to the relay mailbox, full conversation view, and the pairing poll loop.
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
// StatusBar — the 28px strip at the bottom. Surfaces the three bits of
|
|
// information an operator tends to want at a glance:
|
|
// * Connection state to the configured node (poll /api/netstats).
|
|
// * Current chain height (last successful poll).
|
|
// * Node URL (short, hover-tooltip for the full thing).
|
|
//
|
|
// Poll interval is 5 seconds — low enough to feel live, cheap enough
|
|
// that even a free-tier node won't notice.
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useStore } from '@/lib/store';
|
|
import { getNetStats, getNodeUrl, onNodeUrlChange } from '@/lib/api';
|
|
|
|
type ConnState = 'online' | 'connecting' | 'offline';
|
|
|
|
export function StatusBar(): React.ReactElement {
|
|
const nodeUrl = useStore(s => s.settings.nodeUrl);
|
|
const [conn, setConn] = useState<ConnState>('connecting');
|
|
const [height, setHeight] = useState<number | null>(null);
|
|
const [url, setUrl] = useState<string>(getNodeUrl());
|
|
|
|
useEffect(() => onNodeUrlChange(setUrl), []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const poll = async () => {
|
|
try {
|
|
const s = await getNetStats();
|
|
if (cancelled) return;
|
|
setConn('online');
|
|
setHeight(s.total_blocks);
|
|
} catch {
|
|
if (cancelled) return;
|
|
setConn('offline');
|
|
}
|
|
};
|
|
setConn('connecting');
|
|
poll();
|
|
const t = setInterval(poll, 5_000);
|
|
return () => {
|
|
cancelled = true;
|
|
clearInterval(t);
|
|
};
|
|
}, [nodeUrl]);
|
|
|
|
const dot = conn === 'online' ? '#3ba55d' :
|
|
conn === 'connecting' ? '#f0b35a' :
|
|
'#f4212e';
|
|
|
|
const shortUrl = url
|
|
.replace(/^https?:\/\//, '')
|
|
.replace(/\/$/, '');
|
|
|
|
return (
|
|
<footer style={{
|
|
height: 28, minHeight: 28,
|
|
background: '#000',
|
|
borderTop: '1px solid #1f1f1f',
|
|
display: 'flex', alignItems: 'center', padding: '0 16px', gap: 14,
|
|
fontSize: 11, color: '#8b8b8b',
|
|
}}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
|
<span style={{
|
|
width: 8, height: 8, borderRadius: 4, background: dot,
|
|
}} />
|
|
{conn}
|
|
</span>
|
|
<span style={{ opacity: 0.5 }}>·</span>
|
|
<span title={url}>{shortUrl}</span>
|
|
<span style={{ opacity: 0.5 }}>·</span>
|
|
<span>height {height ?? '—'}</span>
|
|
<div style={{ flex: 1 }} />
|
|
</footer>
|
|
);
|
|
}
|