// RequestsList — pending contact requests inbox. // // Each row shows the requester (identity if known + DC address + fee paid) // and their intro message. Accept publishes ACCEPT_CONTACT on-chain, // adds the peer to the local contacts store, and optimistically drops // the row. Reject (Block) publishes BLOCK_CONTACT; subsequent requests // from the same sender are refused by the node. import React, { useState } from 'react'; import { useStore } from '@/lib/store'; import { buildAcceptContactTx, buildBlockContactTx, buildLinkDeviceTx, submitTx, humanizeTxError, } from '@/lib/tx'; import { upsertContact as persistContact, markDeviceRegistered, isDeviceRegistered } from '@/lib/storage'; import { getIdentity, fetchDevices, type ContactRequestRaw } from '@/lib/api'; import { shortAddr } from '@/lib/crypto'; export function RequestsList({ requests, onChanged, }: { requests: ContactRequestRaw[]; onChanged: () => void; }): React.ReactElement { if (requests.length === 0) { return (
No pending requests. Inbound CONTACT_REQUEST txs will show up here for you to accept or block.
); } return (
{requests.map(r => ( ))}
); } function RequestRow({ req, onChanged, }: { req: ContactRequestRaw; onChanged: () => void }) { const keyFile = useStore(s => s.keyFile); const upsertContact = useStore(s => s.upsertContact); const setSection = useStore(s => s.setSection); const setActiveChat = useStore(s => s.setActiveChat); const [busy, setBusy] = useState<'accept' | 'block' | null>(null); const [err, setErr] = useState(null); const act = async (kind: 'accept' | 'block') => { if (!keyFile) return; setBusy(kind); setErr(null); try { if (kind === 'accept') { // Need the requester's X25519 so a local contact is created // with encryption enabled out of the gate — without it the // first outgoing message would surface "no key" until we // refetched via resolveRecipientKeys. const identity = await getIdentity(req.requester_pub); const tx = buildAcceptContactTx({ from: keyFile.pub_key, to: req.requester_pub, privKey: keyFile.priv_key, }); await submitTx(tx); // Make sure OUR device is published on-chain too. The // useDeviceBootstrap effect tries this on sign-in, but if the // user had zero balance then the tx bounced; now that the // incoming CONTACT_REQUEST has paid us the contact fee, we // have the µT needed. Without this, the peer couldn't encrypt // to us — they'd see "recipient has no encryption key" even // though we just accepted. try { const ownDevices = await fetchDevices(keyFile.pub_key); const alreadyLinked = ownDevices.some(d => d.x25519_pub_key === keyFile.x25519_pub); if (!alreadyLinked && !isDeviceRegistered()) { const platform = await window.dchain.app.platform().catch(() => 'unknown'); const deviceName = platform === 'darwin' ? 'Mac' : platform === 'win32' ? 'Windows' : platform === 'linux' ? 'Linux' : 'Desktop'; const linkTx = buildLinkDeviceTx({ from: keyFile.pub_key, x25519Pub: keyFile.x25519_pub, deviceName, privKey: keyFile.priv_key, }); await submitTx(linkTx); markDeviceRegistered(); } } catch { /* best-effort — next sign-in retries */ } const c = { address: req.requester_pub, x25519Pub: identity?.x25519_pub ?? '', username: identity?.nickname || undefined, alias: undefined, addedAt: Date.now(), }; upsertContact(c); persistContact(c); // Jump the user straight into the new chat — mirrors mobile's // router.replace(/chats/) after accept. setActiveChat(req.requester_pub); setSection('messages'); } else { const tx = buildBlockContactTx({ from: keyFile.pub_key, to: req.requester_pub, privKey: keyFile.priv_key, }); await submitTx(tx); } onChanged(); } catch (e) { setErr(humanizeTxError(e)); } finally { setBusy(null); } }; return (
{shortAddr(req.requester_pub, 1).charAt(0).toUpperCase()}
{shortAddr(req.requester_pub, 8)}
{req.requester_addr}
+{(req.fee_ut / 1_000_000).toFixed(3)} T
{req.intro && (
{req.intro}
)}
{err && (
{err}
)}
); }