Two bugs reported by the user:
1. After accepting a contact request on the desktop, the requester's
"Send message" call errored with "no encryption key published" for
the newly-accepted contact. Root cause: desktop never ran the
device-registry bootstrap (mobile does it from _layout.tsx on
sign-in) — so the desktop's X25519 pub was never published via
LINK_DEVICE, and resolveRecipientKeys returned an empty list.
2. On the accepting device, the new chat didn't appear in Messages
after tapping Accept — accept wrote the contact to store + disk
but didn't switch sections, so the user was stuck in Contacts
watching nothing happen.
Fixes:
* hooks/useDeviceBootstrap — direct port of mobile's _layout.tsx
bootstrap effect. On every sign-in:
- fetchDevices(master) → if our X25519 is listed, mark local
registered flag.
- not listed + was registered before → REVOKED → wipe state +
bounce to Welcome.
- not listed + never registered → submit LINK_DEVICE. Tx may
bounce if balance is zero; next launch retries.
Mounted from App.tsx so it runs once per authenticated session.
* RequestsList.accept — after submitting ACCEPT_CONTACT, check if
OUR X25519 is in the on-chain registry. If not, submit LINK_DEVICE
immediately (balance is now covered by the contact fee the peer
paid us). This closes the window where the peer couldn't encrypt
to us because our key wasn't published yet.
Also: after a successful accept, setSection('messages') +
setActiveChat(requester_pub), matching mobile's
router.replace('/chats/<pub>') flow.
* Conversation.send — nicer error copy when
resolveRecipientKeys returns []. Was: "recipient has no
encryption key published". Now: actionable text asking the peer
to re-open their app so the LINK_DEVICE tx commits.
Closes the v2.2.0 roadmap. Desktop client is feature-complete and
ready for first installer builds.
Contact request flow (fills a real gap flagged by the user):
* lib/tx.ts grows buildContactRequestTx / buildAcceptContactTx /
buildBlockContactTx with canonical bytes matching mobile.
* lib/api.ts: fetchContactRequests + ContactRequestRaw.
* New contact modal — sections/contacts/NewContactModal.tsx — resolves
@username / DC-address / hex pub via resolveAccount, shows identity
preview (incl. "has encryption key / key not published" hint),
fee tier picker (5k / 10k / 50k µT), optional 280-char intro,
balance guard.
* Requests inbox — sections/contacts/RequestsList.tsx — polled every
15 s via /relay/contacts, filters pending, Accept submits
ACCEPT_CONTACT + adds the peer to local contacts with their
identity.x25519_pub pre-cached, Block submits BLOCK_CONTACT.
* ContactsList grows a two-tab header (Contacts / Requests with a
pending-count badge) + "+ New" button next to the filter input.
Auto-update:
* hooks/useUpdateCheck.ts — polls /api/update-check on mount and
every 6 hours; loose semver compares the Gitea release tag
against this build's app.version (from Electron IPC), ignores
the node's own update_available flag (it compares vs. the node,
not the desktop).
* shell/UpdateBanner.tsx — thin strip above the status bar with
the new tag, Download button (opens the release URL in the
default browser), and a dismiss-for-this-tag × so once-seen
updates don't nag.
Packaging — electron-builder config tightened:
* artifactName pattern includes version + os + arch.
* Mac: hardenedRuntime on, dmg + zip outputs, social-networking
category.
* Windows: NSIS (full installer, per-user or per-machine) +
portable exe.
* Linux: AppImage + deb.
* Strip source maps and test folders from the asar.
* publish: null — no auto-publisher yet; Gitea releases are
uploaded manually for now.
* directories.output = release/, directories.buildResources =
resources/ so icons land in a predictable place once we add them.
Version bumped to 2.2.0 in package.json. docs/ROADMAP.md marks
v2.2.0 row complete; remaining work (attachments, code signing,
group chats) moved to a post-v2.2.0 bucket.