feat(desktop): Contacts + Settings→Devices + expanded Profile + QR + keybinds (v2.2.0-rc1)
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.
This commit is contained in:
61
desktop/src/sections/settings/SettingsNav.tsx
Normal file
61
desktop/src/sections/settings/SettingsNav.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user