Completes the desktop feature surface ahead of the v2.2.0 tag. Only
auto-update + packaging remain.
Settings — now two-paned (nav on the left, pages on the right):
* NodePage — URL ping-on-commit + API token field.
* IdentityPage — pub key / X25519 pub, Export (safe-save dialog) /
Import (open dialog + wipe + replace) / Delete identity.
* DevicesPage — full multi-device UI: list every active device with
a THIS DEVICE badge; Unlink button on every other row submits
UNLINK_DEVICE + optimistic local remove; Link new device modal
takes {code, device key, name}, submits LINK_DEVICE, then ships
the handshake envelope (master Ed25519 priv encrypted for the
new X25519) — same protocol as mobile's primary-device modal.
* AboutPage — version, platform, Gitea links.
* store.settingsPage discriminated union keeps selection across
section switches.
Contacts section (now real):
* ContactsList — alphabetical, filter-as-you-type; each row shows
avatar letter + name + short address.
* ContactsDetail — profile card (username/alias/pub) + Open chat /
View posts / Copy address actions + stats grid
(Balance, Devices, Encryption, Added) + Identity card with
DC address, username, published X25519, device_count.
* store.selectedContact persists across navigation.
Profile section (expanded):
* ProfileList — big avatar + pub key + contacts count.
* ProfileDetail — balance hero, quick actions (My posts →
feed author wall, Manage devices → Settings→Devices, Copy
address), Identity card, inline Linked devices list with a
THIS DEVICE badge matching the Settings page.
Receive modal — canvas QR via `qrcode` (new dep, ~5 KB gzipped),
white-on-transparent so it sits inside the same black modal chrome.
Global keybinds (useGlobalKeybinds hook mounted in Shell):
* Ctrl/Cmd+W — close the current conversation (drops activeChat,
keeps section). Does NOT close the window.
* Ctrl/Cmd+K — jump to Contacts.
* Ctrl/Cmd+, — Settings.
Each guards against being in a text field so typing `k,` in a
composer / search doesn't hijack.
docs/ROADMAP.md — rc1 row flipped to done; v2.2.0 narrows to
auto-update + packaging + optional attachments in Compose.
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
// Left-pane category list for Settings. Keeps selection in
|
|
// store.settingsPage so switching away and back preserves place.
|
|
|
|
import React from 'react';
|
|
import { useStore, type SettingsPage } from '@/lib/store';
|
|
|
|
interface Row {
|
|
key: SettingsPage;
|
|
label: string;
|
|
hint: string;
|
|
}
|
|
|
|
const ROWS: Row[] = [
|
|
{ key: 'node', label: 'Node', hint: 'URL, connection status' },
|
|
{ key: 'identity', label: 'Identity', hint: 'Your keys and address' },
|
|
{ key: 'devices', label: 'Devices', hint: 'Linked devices, pair a new one' },
|
|
{ key: 'about', label: 'About', hint: 'Version, links' },
|
|
];
|
|
|
|
export function SettingsNav(): React.ReactElement {
|
|
const page = useStore(s => s.settingsPage);
|
|
const setPage = useStore(s => s.setSettingsPage);
|
|
return (
|
|
<div style={{ padding: 10 }}>
|
|
{ROWS.map(r => (
|
|
<NavEntry
|
|
key={r.key}
|
|
label={r.label}
|
|
hint={r.hint}
|
|
active={page === r.key}
|
|
onClick={() => setPage(r.key)}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NavEntry({
|
|
label, hint, active, onClick,
|
|
}: { label: string; hint: string; active: boolean; onClick: () => void }) {
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
style={{
|
|
padding: '10px 12px', borderRadius: 10, cursor: 'pointer',
|
|
background: active ? '#0a1a29' : 'transparent',
|
|
border: active ? '1px solid #1d9bf022' : '1px solid transparent',
|
|
}}
|
|
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLDivElement).style.background = '#0a0a0a'; }}
|
|
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLDivElement).style.background = 'transparent'; }}
|
|
>
|
|
<div style={{
|
|
color: active ? '#1d9bf0' : '#fff',
|
|
fontSize: 14, fontWeight: 700,
|
|
}}>{label}</div>
|
|
<div style={{ color: '#6a6a6a', fontSize: 11, marginTop: 2 }}>
|
|
{hint}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|