mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-11 00:27:55 +02:00
Remove dead Drop dashboard UI so Agent Shell frontend build passes.
Dead Drop chat stays in Infonet Terminal; Mesh Chat dms tab is Agent Shell only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1492,778 +1492,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Dead Drop chat UI moved to Infonet Terminal → Messages */}
|
||||
{false && !dashboardRestrictedTab && activeTab === 'dms' && (
|
||||
<>
|
||||
{/* Sub-nav: Contacts | Inbox | Muted | (back to contacts from chat) */}
|
||||
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-[var(--border-primary)]/30 shrink-0">
|
||||
{dmView === 'chat' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDmView('contacts');
|
||||
setSelectedContact('');
|
||||
setDmMessages([]);
|
||||
}}
|
||||
className="text-[13px] font-mono text-[var(--text-muted)] hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
< BACK
|
||||
</button>
|
||||
<span className="text-sm font-mono text-cyan-400 ml-2 truncate">
|
||||
{selectedContact.slice(0, 16)}
|
||||
</span>
|
||||
{(() => {
|
||||
const c = contacts[selectedContact];
|
||||
if (!c) return null;
|
||||
const trust = getContactTrustSummary(c);
|
||||
if (trust?.transparencyConflict) {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
|
||||
HISTORY CONFLICT
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (trust?.state === 'continuity_broken') {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
|
||||
CONTINUITY BROKEN
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (trust?.state === 'mismatch') {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-orange-500/40 text-orange-300 bg-orange-950/20">
|
||||
PREKEY CHANGED
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (trust?.registryMismatch) {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-400 bg-red-950/20">
|
||||
KEY MISMATCH
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (trust?.state === 'sas_verified') {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-green-500/40 text-green-400 bg-green-950/20">
|
||||
SAS VERIFIED
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (trust?.state === 'invite_pinned') {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-emerald-500/40 text-emerald-300 bg-emerald-950/20">
|
||||
INVITE PINNED
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (trust?.state === 'tofu_pinned') {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-amber-500/30 text-amber-300 bg-amber-950/10">
|
||||
TOFU ONLY
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{(() => {
|
||||
const c = contacts[selectedContact];
|
||||
if (!c) return null;
|
||||
if (c.witness_count && c.witness_count > 0) {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-cyan-500/30 text-cyan-300 bg-cyan-950/10">
|
||||
WITNESSED {c.witness_count}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{(() => {
|
||||
const c = contacts[selectedContact];
|
||||
if (!c) return null;
|
||||
if (c.vouch_count && c.vouch_count > 0) {
|
||||
return (
|
||||
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-purple-500/30 text-purple-300 bg-purple-950/10">
|
||||
VOUCHES {c.vouch_count}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
<button
|
||||
onClick={handleDmTrustPrimaryAction}
|
||||
className="ml-auto text-[12px] font-mono px-2 py-0.5 border border-cyan-800/40 text-cyan-400/90 hover:text-cyan-300 hover:border-cyan-600/60 transition-colors"
|
||||
>
|
||||
{dmTrustPrimaryButtonLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleVouch(selectedContact)}
|
||||
className="ml-2 text-[12px] font-mono px-2 py-0.5 border border-purple-800/40 text-purple-400/90 hover:text-purple-300 hover:border-purple-600/60 transition-colors"
|
||||
>
|
||||
VOUCH
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handleRefreshSelectedContact()}
|
||||
disabled={dmMaintenanceBusy}
|
||||
className="ml-2 text-[12px] font-mono px-2 py-0.5 border border-amber-800/40 text-amber-300/90 hover:text-amber-200 hover:border-amber-600/60 transition-colors disabled:opacity-40"
|
||||
>
|
||||
REFRESH
|
||||
</button>
|
||||
<button
|
||||
onClick={() => void handleResetSelectedContact()}
|
||||
disabled={dmMaintenanceBusy}
|
||||
className="ml-2 text-[12px] font-mono px-2 py-0.5 border border-red-800/40 text-red-300/90 hover:text-red-200 hover:border-red-600/60 transition-colors disabled:opacity-40"
|
||||
>
|
||||
RESET
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setDmView('contacts')}
|
||||
className={`text-[13px] font-mono px-2 py-0.5 transition-colors ${
|
||||
dmView === 'contacts'
|
||||
? 'text-cyan-400 bg-cyan-950/30'
|
||||
: 'text-[var(--text-muted)] hover:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
CONTACTS
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDmView('inbox')}
|
||||
className={`text-[13px] font-mono px-2 py-0.5 transition-colors flex items-center gap-1 ${
|
||||
dmView === 'inbox'
|
||||
? 'text-cyan-400 bg-cyan-950/30'
|
||||
: 'text-[var(--text-muted)] hover:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
INBOX
|
||||
{accessRequests.length > 0 && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-[blink_1s_step-end_infinite]" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDmView('muted')}
|
||||
className={`text-[13px] font-mono px-2 py-0.5 transition-colors flex items-center gap-1 ${
|
||||
dmView === 'muted'
|
||||
? 'text-cyan-400 bg-cyan-950/30'
|
||||
: 'text-[var(--text-muted)] hover:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<EyeOff size={8} />
|
||||
MUTED
|
||||
{mutedArray.length > 0 && (
|
||||
<span className="text-[11px] text-[var(--text-muted)]">
|
||||
({mutedArray.length})
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddContact(!showAddContact)}
|
||||
disabled={secureDmBlocked}
|
||||
className="ml-auto p-1 hover:bg-[var(--hover-accent)] text-[var(--text-muted)] hover:text-cyan-400 transition-colors"
|
||||
title="Request access"
|
||||
>
|
||||
<UserPlus size={11} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{dmView === 'chat' && showSas && sasPhrase && (
|
||||
<div className="px-3 pb-1 text-[13px] font-mono text-cyan-400/80 border-b border-[var(--border-primary)]/20">
|
||||
SAS: <span className="text-cyan-300">{sasPhrase}</span>
|
||||
{selectedContactInfo &&
|
||||
selectedContactTrustSummary?.state === 'invite_pinned' && (
|
||||
<div className="mt-1 text-[12px] font-mono text-emerald-300/90 leading-[1.65]">
|
||||
This contact was anchored by an imported signed invite. SAS is still useful
|
||||
as an extra continuity check.
|
||||
</div>
|
||||
)}
|
||||
{selectedContactInfo &&
|
||||
selectedContactTrustSummary?.state === 'tofu_pinned' && (
|
||||
<div className="mt-1 text-[12px] font-mono text-amber-300/90 leading-[1.65]">
|
||||
First contact is still TOFU-only. Compare this phrase out of band before
|
||||
treating the sender as verified.
|
||||
</div>
|
||||
)}
|
||||
{selectedContactInfo &&
|
||||
selectedContactTrustSummary?.state !== 'sas_verified' &&
|
||||
selectedContactTrustSummary?.state !== 'mismatch' &&
|
||||
selectedContactTrustSummary?.state !== 'continuity_broken' &&
|
||||
!selectedContactTrustSummary?.transparencyConflict && (
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<input
|
||||
value={sasConfirmInput}
|
||||
onChange={(e) => setSasConfirmInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void handleConfirmSelectedContactSas();
|
||||
}
|
||||
}}
|
||||
placeholder="Type the phrase you both confirmed"
|
||||
className="flex-1 min-w-0 bg-black/30 border border-cyan-900/30 px-2 py-1 text-[12px] font-mono text-cyan-100 placeholder:text-cyan-700/70 focus:outline-none focus:border-cyan-600/60"
|
||||
/>
|
||||
<button
|
||||
onClick={() => void handleConfirmSelectedContactSas()}
|
||||
disabled={dmMaintenanceBusy}
|
||||
className="text-[12px] font-mono px-2 py-1 border border-emerald-800/40 text-emerald-300 hover:text-emerald-200 hover:border-emerald-600/60 transition-colors disabled:opacity-40"
|
||||
>
|
||||
CONFIRM SAS
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedContactInfo &&
|
||||
selectedContactTrustSummary?.state === 'continuity_broken' &&
|
||||
selectedContactTrustSummary?.rootMismatch && (
|
||||
<>
|
||||
<div className="mt-1 text-[12px] font-mono text-red-300/90 leading-[1.65]">
|
||||
{`${rootWitnessContinuityLabel(selectedContactTrustSummary)} changed for this contact.`}{' '}
|
||||
Compare the SAS phrase for the newly observed root, then recover only if
|
||||
the ceremony checks out.
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<input
|
||||
value={sasConfirmInput}
|
||||
onChange={(e) => setSasConfirmInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void handleRecoverSelectedContactRootContinuity();
|
||||
}
|
||||
}}
|
||||
placeholder="Type the phrase you both confirmed for the new root"
|
||||
className="flex-1 min-w-0 bg-black/30 border border-red-900/30 px-2 py-1 text-[12px] font-mono text-cyan-100 placeholder:text-red-700/70 focus:outline-none focus:border-red-600/60"
|
||||
/>
|
||||
<button
|
||||
onClick={() => void handleRecoverSelectedContactRootContinuity()}
|
||||
disabled={dmMaintenanceBusy}
|
||||
className="text-[12px] font-mono px-2 py-1 border border-red-800/40 text-red-300 hover:text-red-200 hover:border-red-600/60 transition-colors disabled:opacity-40"
|
||||
>
|
||||
RECOVER ROOT
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{selectedContactInfo?.remotePrekeyMismatch && (
|
||||
<div className="mt-2 text-[12px] font-mono text-red-300/85 leading-[1.65]">
|
||||
{selectedContactTrustSummary?.rootMismatch
|
||||
? `${rootWitnessContinuityLabel(selectedContactTrustSummary)} changed. Recover only after you compare the new SAS phrase out of band.`
|
||||
: 'Acknowledge the changed fingerprint first, then compare and confirm SAS again.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'dms' && !secureDmBlocked && (
|
||||
<div className="px-3 py-1.5 border-b border-[var(--border-primary)]/20 shrink-0 flex items-center gap-2">
|
||||
<span
|
||||
className={`text-[12px] font-mono px-1.5 py-0.5 border ${dmTransportStatus.className}`}
|
||||
>
|
||||
{dmTransportStatus.label}
|
||||
</span>
|
||||
<span className="text-[12px] font-mono text-[var(--text-muted)]">
|
||||
{dmTransportMode === 'reticulum'
|
||||
? 'Direct private delivery active.'
|
||||
: dmTransportMode === 'hidden'
|
||||
? 'Hidden transport active.'
|
||||
: dmTransportMode === 'relay'
|
||||
? 'Relay fallback active.'
|
||||
: dmTransportMode === 'ready'
|
||||
? 'Private lane ready.'
|
||||
: 'Lower-trust mode.'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'dms' && unresolvedSenderSealCount > 0 && (
|
||||
<div className="px-3 py-2 border-b border-red-900/30 bg-red-950/18 text-red-300 leading-[1.65] shrink-0">
|
||||
<div className="text-[13px] font-mono tracking-[0.18em] mb-1">
|
||||
UNRESOLVED SEALED SENDERS
|
||||
</div>
|
||||
<div className="text-sm font-mono">
|
||||
{unresolvedSenderSealCount} sealed-sender message
|
||||
{unresolvedSenderSealCount === 1 ? '' : 's'} could not be mapped to a
|
||||
trusted contact or verified sender key. Keep Wormhole reachable and refresh
|
||||
contact trust before relying on them.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'dms' && dmView === 'chat' && dmTrustHint && selectedContactInfo && (
|
||||
<div
|
||||
className={`px-3 py-2 border-b leading-[1.65] shrink-0 ${
|
||||
dmTrustHint.severity === 'danger'
|
||||
? 'border-red-900/30 bg-red-950/20 text-red-300'
|
||||
: 'border-amber-900/30 bg-amber-950/10 text-amber-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[13px] font-mono tracking-[0.18em] mb-1">
|
||||
{dmTrustHint.title}
|
||||
</div>
|
||||
<div className="text-sm font-mono">{dmTrustHint.detail}</div>
|
||||
{selectedContactInfo.remotePrekeyMismatch && (
|
||||
<div className="mt-2 text-[13px] font-mono text-red-200/85">
|
||||
pinned {shortTrustFingerprint(selectedContactInfo.remotePrekeyFingerprint)} • observed{' '}
|
||||
{shortTrustFingerprint(selectedContactInfo.remotePrekeyObservedFingerprint)}
|
||||
</div>
|
||||
)}
|
||||
{!selectedContactInfo.remotePrekeyMismatch &&
|
||||
selectedContactInfo.remotePrekeyRootMismatch && (
|
||||
<div className="mt-2 text-[13px] font-mono text-red-200/85">
|
||||
pinned root {shortTrustFingerprint(selectedContactInfo.remotePrekeyRootFingerprint)} •
|
||||
observed root{' '}
|
||||
{shortTrustFingerprint(selectedContactInfo.remotePrekeyObservedRootFingerprint)}
|
||||
</div>
|
||||
)}
|
||||
{!selectedContactInfo.remotePrekeyMismatch &&
|
||||
selectedContactTrustSummary?.state === 'tofu_pinned' &&
|
||||
selectedContactInfo.remotePrekeyFingerprint && (
|
||||
<div className="mt-2 text-[13px] font-mono text-amber-200/85">
|
||||
first-sight pin {shortTrustFingerprint(selectedContactInfo.remotePrekeyFingerprint)} •
|
||||
verify before sensitive use
|
||||
</div>
|
||||
)}
|
||||
{!selectedContactInfo.remotePrekeyMismatch &&
|
||||
selectedContactTrustSummary?.state === 'invite_pinned' &&
|
||||
(selectedContactInfo.invitePinnedTrustFingerprint ||
|
||||
selectedContactInfo.remotePrekeyFingerprint) && (
|
||||
<div className="mt-2 text-[13px] font-mono text-emerald-200/85">
|
||||
invite pin{' '}
|
||||
{shortTrustFingerprint(
|
||||
selectedContactInfo.invitePinnedTrustFingerprint ||
|
||||
selectedContactInfo.remotePrekeyFingerprint,
|
||||
)}{' '}
|
||||
•
|
||||
{selectedContactTrustSummary?.rootAttested &&
|
||||
(selectedContactInfo.invitePinnedRootFingerprint ||
|
||||
selectedContactInfo.remotePrekeyRootFingerprint)
|
||||
? ` ${rootWitnessBadgeLabel(selectedContactTrustSummary).toLowerCase()} ${shortTrustFingerprint(
|
||||
selectedContactInfo.invitePinnedRootFingerprint ||
|
||||
selectedContactInfo.remotePrekeyRootFingerprint,
|
||||
)} •`
|
||||
: ''}{' '}
|
||||
imported out of band before first contact
|
||||
</div>
|
||||
)}
|
||||
{selectedContactTrustSummary?.state === 'continuity_broken' &&
|
||||
selectedContactTrustSummary?.rootMismatch && (
|
||||
<div className="mt-2 text-[13px] font-mono text-red-200/85 leading-[1.7]">
|
||||
{`${rootWitnessContinuityLabel(selectedContactTrustSummary).toLowerCase()} broke for this contact.`}{' '}
|
||||
Re-verify SAS or replace the signed invite before trusting the new
|
||||
root.
|
||||
</div>
|
||||
)}
|
||||
{selectedContactInfo.remotePrekeyTransparencyConflict && (
|
||||
<div className="mt-2 text-[13px] font-mono text-red-200/85 leading-[1.7]">
|
||||
prekey history conflict observed and trust stays degraded until you
|
||||
explicitly acknowledge the changed fingerprint.
|
||||
</div>
|
||||
)}
|
||||
{selectedContactInfo.remotePrekeyLookupMode === 'legacy_agent_id' && (
|
||||
<div className="mt-2 text-[13px] font-mono text-yellow-200/85 leading-[1.7]">
|
||||
bootstrap path: legacy direct agent ID lookup.
|
||||
{selectedContactInfo.invitePinnedPrekeyLookupHandle
|
||||
? ' Refresh from the signed invite to tighten lookup privacy.'
|
||||
: ' Import or re-import a signed invite to avoid stable-ID lookup.'}
|
||||
</div>
|
||||
)}
|
||||
{selectedContactInfo.remotePrekeyLookupMode === 'invite_lookup_handle' && (
|
||||
<div className="mt-2 text-[13px] font-mono text-cyan-200/85 leading-[1.7]">
|
||||
bootstrap path: invite-scoped lookup handle. Stable agent ID was not
|
||||
required on the lookup path.
|
||||
</div>
|
||||
)}
|
||||
{(selectedContactInfo.witness_count ?? 0) > 0 && (
|
||||
<div className="mt-2 text-[13px] font-mono text-cyan-200/75 leading-[1.7]">
|
||||
witness observations: {selectedContactInfo.witness_count}
|
||||
{selectedContactInfo.witness_checked_at
|
||||
? `, last seen ${timeAgo(
|
||||
selectedContactInfo.witness_checked_at > 1_000_000_000_000
|
||||
? selectedContactInfo.witness_checked_at
|
||||
: selectedContactInfo.witness_checked_at * 1000,
|
||||
)}`
|
||||
: ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<button
|
||||
onClick={handleDmTrustPrimaryAction}
|
||||
className="text-[12px] font-mono px-2 py-0.5 border border-cyan-800/40 text-cyan-300 hover:text-cyan-200 hover:border-cyan-600/60 transition-colors"
|
||||
>
|
||||
{dmTrustPrimaryButtonLabel}
|
||||
</button>
|
||||
{selectedContactInfo.remotePrekeyMismatch &&
|
||||
!selectedContactTrustSummary?.rootMismatch && (
|
||||
<button
|
||||
onClick={() => void handleTrustSelectedRemotePrekey()}
|
||||
disabled={dmMaintenanceBusy}
|
||||
className="text-[12px] font-mono px-2 py-0.5 border border-orange-700/40 text-orange-300 hover:text-orange-200 hover:border-orange-500/60 transition-colors disabled:opacity-40"
|
||||
>
|
||||
TRUST NEW KEY
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add contact / request access form */}
|
||||
<AnimatePresence>
|
||||
{showAddContact && dmView !== 'chat' && !secureDmBlocked && (
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: 'auto' }}
|
||||
exit={{ height: 0 }}
|
||||
className="overflow-hidden border-b border-[var(--border-primary)]/30 shrink-0"
|
||||
>
|
||||
<div className="px-3 py-2 space-y-1.5">
|
||||
<div className="text-[13px] font-mono text-[var(--text-muted)] leading-[1.65]">
|
||||
Enter an Agent ID for a contact you already pinned with a signed invite
|
||||
to request Dead Drop access. If you only have older local state, use
|
||||
terminal <span className="text-yellow-400">dm add</span> only for
|
||||
legacy migration.
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
value={addContactId}
|
||||
onChange={(e) => setAddContactId(e.target.value)}
|
||||
placeholder="!sb_a3f2c891..."
|
||||
className="flex-1 bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] text-sm font-mono text-cyan-300 px-2 py-1 outline-none placeholder:text-[var(--text-muted)]"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleRequestComposerAction();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleRequestComposerAction}
|
||||
disabled={!addContactId.trim() || !hasId}
|
||||
className="text-[13px] font-mono px-2 py-1 bg-cyan-900/20 text-cyan-400 hover:bg-cyan-800/30 disabled:opacity-30 transition-colors"
|
||||
>
|
||||
REQUEST
|
||||
</button>
|
||||
</div>
|
||||
{pendingSent.includes(addContactId.trim()) && (
|
||||
<div className="text-[13px] font-mono text-yellow-500/70">
|
||||
Request already sent
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 overflow-y-auto styled-scrollbar px-3 py-1.5 space-y-0.5 border-l-2 border-cyan-800/25">
|
||||
{secureDmBlocked && (
|
||||
<div className="flex h-full min-h-[220px] items-center justify-center py-6">
|
||||
<div className="max-w-sm w-full border border-cyan-900/30 bg-cyan-950/10 px-4 py-5 text-center">
|
||||
<div className="inline-flex items-center justify-center w-10 h-10 border border-cyan-700/40 bg-black/30 text-cyan-300 mb-3">
|
||||
<Lock size={16} />
|
||||
</div>
|
||||
<div className="text-sm font-mono tracking-[0.24em] text-cyan-300 mb-2">
|
||||
DEAD DROP LOCKED
|
||||
</div>
|
||||
<div className="text-sm font-mono text-[var(--text-secondary)] leading-[1.7]">
|
||||
Need Wormhole activated.
|
||||
</div>
|
||||
<div className="mt-2 text-[13px] font-mono text-cyan-300/70">
|
||||
Contacts, inbox, and private messages unlock once the private lane is up.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CONTACTS VIEW */}
|
||||
{!secureDmBlocked && dmView === 'contacts' && (
|
||||
<>
|
||||
{contactList.length === 0 && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
No contacts yet. Use <span className="text-cyan-500/70">+</span> to
|
||||
request access.
|
||||
</div>
|
||||
)}
|
||||
{contactList.map(([id, c]) => {
|
||||
const trust = getContactTrustSummary(c);
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center gap-2 py-1.5 border-b border-[var(--border-primary)]/30 last:border-0 cursor-pointer hover:bg-[var(--bg-secondary)]/50 px-1 -mx-1 transition-colors"
|
||||
onClick={() => openChat(id)}
|
||||
>
|
||||
<Lock size={10} className="text-[var(--text-muted)] shrink-0" />
|
||||
<span className="text-sm font-mono text-cyan-300 truncate">
|
||||
{c.alias || id.slice(0, 16)}
|
||||
</span>
|
||||
{c.remotePrekeyMismatch && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-orange-500/40 text-orange-300 bg-orange-950/20">
|
||||
REVERIFY
|
||||
</span>
|
||||
)}
|
||||
{!c.remotePrekeyMismatch && c.verify_mismatch && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
|
||||
MISMATCH
|
||||
</span>
|
||||
)}
|
||||
{!c.remotePrekeyMismatch && !c.verify_mismatch && trust?.state === 'invite_pinned' && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-emerald-500/40 text-emerald-300 bg-emerald-950/20">
|
||||
INVITE PINNED
|
||||
</span>
|
||||
)}
|
||||
{!c.remotePrekeyMismatch && !c.verify_mismatch && trust?.state === 'sas_verified' && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-green-500/40 text-green-400 bg-green-950/20">
|
||||
SAS VERIFIED
|
||||
</span>
|
||||
)}
|
||||
{!c.remotePrekeyMismatch &&
|
||||
!c.verify_mismatch &&
|
||||
!c.remotePrekeyTransparencyConflict &&
|
||||
c.remotePrekeyLookupMode === 'legacy_agent_id' && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-yellow-500/30 text-yellow-300 bg-yellow-950/10">
|
||||
LEGACY LOOKUP
|
||||
</span>
|
||||
)}
|
||||
{!c.remotePrekeyMismatch && !c.verify_mismatch && c.remotePrekeyTransparencyConflict && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
|
||||
HISTORY CONFLICT
|
||||
</span>
|
||||
)}
|
||||
{!c.remotePrekeyMismatch &&
|
||||
!c.verify_mismatch &&
|
||||
trust?.state === 'tofu_pinned' && (
|
||||
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-amber-500/30 text-amber-300 bg-amber-950/10">
|
||||
TOFU ONLY
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleBlockDM(id);
|
||||
}}
|
||||
className="ml-auto p-0.5 text-[var(--text-muted)] hover:text-red-400 hover:bg-red-900/20 transition-colors"
|
||||
title="Block"
|
||||
>
|
||||
<Ban size={10} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{pendingSent.length > 0 && (
|
||||
<>
|
||||
<div className="text-[13px] font-mono text-[var(--text-muted)] mt-2 mb-1">
|
||||
PENDING SENT
|
||||
</div>
|
||||
{pendingSent.map((id) => (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center gap-2 py-1 text-sm font-mono text-[var(--text-muted)]"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-yellow-600/50" />
|
||||
<span className="truncate">{id.slice(0, 16)}</span>
|
||||
<span className="ml-auto text-[12px] text-[var(--text-muted)]">
|
||||
awaiting
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* INBOX VIEW — access requests */}
|
||||
{!secureDmBlocked && dmView === 'inbox' && (
|
||||
<>
|
||||
{accessRequests.length === 0 && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
No incoming requests
|
||||
</div>
|
||||
)}
|
||||
{accessRequests.map((req) => {
|
||||
const requestActionsAllowed = shouldAllowRequestActions(req);
|
||||
const recoveryState = req.sender_recovery_state;
|
||||
return (
|
||||
<div
|
||||
key={req.sender_id}
|
||||
className="py-2 border-b border-[var(--border-primary)]/30 last:border-0"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<UserPlus size={10} className="text-cyan-500 shrink-0" />
|
||||
<span className="text-sm font-mono text-cyan-300 truncate">
|
||||
{req.sender_id.slice(0, 16)}
|
||||
</span>
|
||||
{recoveryState === 'verified' && (
|
||||
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-green-500/30 text-green-400 bg-green-950/20">
|
||||
VERIFIED
|
||||
</span>
|
||||
)}
|
||||
{recoveryState === 'pending' && (
|
||||
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-yellow-500/30 text-yellow-300 bg-yellow-950/20">
|
||||
RECOVERY PENDING
|
||||
</span>
|
||||
)}
|
||||
{recoveryState === 'failed' && (
|
||||
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-red-500/30 text-red-300 bg-red-950/20">
|
||||
RECOVERY FAILED
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[12px] font-mono text-[var(--text-muted)] ml-auto shrink-0">
|
||||
{timeAgo(req.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[13px] font-mono text-[var(--text-muted)] mt-0.5 leading-[1.65]">
|
||||
Requesting Dead Drop access
|
||||
</div>
|
||||
{req.geo_hint && (
|
||||
<div className="text-[12px] font-mono text-[var(--text-muted)] mt-0.5">
|
||||
Geo hint (not proof): {req.geo_hint}
|
||||
</div>
|
||||
)}
|
||||
{!requestActionsAllowed && (
|
||||
<div className="text-[12px] font-mono text-yellow-300 mt-0.5 leading-[1.65]">
|
||||
Sender authority is not verified yet. Actions stay disabled until
|
||||
local recovery succeeds.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 mt-1.5">
|
||||
<button
|
||||
onClick={() => handleAcceptRequest(req.sender_id)}
|
||||
disabled={!requestActionsAllowed}
|
||||
className={`flex items-center gap-1 text-[13px] font-mono px-2 py-0.5 transition-colors ${
|
||||
requestActionsAllowed
|
||||
? 'bg-cyan-900/20 text-cyan-400 hover:bg-cyan-800/30'
|
||||
: 'bg-cyan-950/10 text-cyan-700 cursor-not-allowed opacity-50'
|
||||
}`}
|
||||
>
|
||||
<Check size={9} /> ACCEPT
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDenyRequest(req.sender_id)}
|
||||
disabled={!requestActionsAllowed}
|
||||
className={`flex items-center gap-1 text-[13px] font-mono px-2 py-0.5 transition-colors ${
|
||||
requestActionsAllowed
|
||||
? 'bg-gray-900/30 text-gray-400 hover:bg-gray-800/40'
|
||||
: 'bg-gray-950/20 text-gray-600 cursor-not-allowed opacity-50'
|
||||
}`}
|
||||
>
|
||||
<X size={9} /> DENY
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleBlockDM(req.sender_id)}
|
||||
disabled={!requestActionsAllowed}
|
||||
className={`flex items-center gap-1 text-[13px] font-mono px-2 py-0.5 ml-auto transition-colors ${
|
||||
requestActionsAllowed
|
||||
? 'text-[var(--text-muted)] hover:text-red-400 hover:bg-red-900/20'
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<Ban size={9} /> BLOCK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* MUTED LIST VIEW */}
|
||||
{!secureDmBlocked && dmView === 'muted' && (
|
||||
<>
|
||||
{mutedArray.length === 0 && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
No muted users
|
||||
</div>
|
||||
)}
|
||||
{mutedArray.map((uid) => (
|
||||
<div
|
||||
key={uid}
|
||||
className="flex items-center gap-2 py-1.5 border-b border-[var(--border-primary)]/30 last:border-0 px-1 -mx-1"
|
||||
>
|
||||
<EyeOff size={10} className="text-[var(--text-muted)] shrink-0" />
|
||||
<span className="text-sm font-mono text-[var(--text-secondary)] truncate flex-1">
|
||||
{uid.slice(0, 20)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleUnmute(uid)}
|
||||
className="flex items-center gap-1 text-[12px] font-mono px-2 py-0.5 bg-cyan-900/20 text-cyan-500 hover:bg-cyan-800/30 transition-colors"
|
||||
>
|
||||
<Eye size={8} /> UNMUTE
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* CHAT VIEW */}
|
||||
{!secureDmBlocked && dmView === 'chat' && (
|
||||
<>
|
||||
{dmMessages.length === 0 && (
|
||||
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
|
||||
<Lock size={11} className="inline mr-1 mb-0.5" />
|
||||
E2E encrypted dead drop — no messages yet
|
||||
</div>
|
||||
)}
|
||||
{dmMessages.map((m) => (
|
||||
<div key={m.msg_id} className="py-0.5 leading-[1.65]">
|
||||
<div className="flex gap-1.5 text-sm font-mono">
|
||||
<span
|
||||
className={`shrink-0 ${
|
||||
m.sender_id === identity?.nodeId
|
||||
? 'text-cyan-500'
|
||||
: 'text-cyan-400'
|
||||
}`}
|
||||
>
|
||||
{m.sender_id === identity?.nodeId
|
||||
? 'you'
|
||||
: m.sender_id.slice(0, 12)}
|
||||
</span>
|
||||
{m.sender_id !== identity?.nodeId && m.seal_verified === true && (
|
||||
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-green-500/30 text-green-400 bg-green-950/20">
|
||||
VERIFIED
|
||||
</span>
|
||||
)}
|
||||
{m.sender_id !== identity?.nodeId && m.seal_resolution_failed && (
|
||||
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-red-500/30 text-red-300 bg-red-950/20">
|
||||
SEAL UNRESOLVED
|
||||
</span>
|
||||
)}
|
||||
{m.sender_id !== identity?.nodeId &&
|
||||
!m.seal_resolution_failed &&
|
||||
m.seal_verified === false && (
|
||||
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-red-500/30 text-red-400 bg-red-950/20">
|
||||
UNVERIFIED
|
||||
</span>
|
||||
)}
|
||||
{m.transport && (
|
||||
<span
|
||||
className={`text-[12px] font-mono px-1.5 py-0.5 border ${
|
||||
m.transport === 'reticulum'
|
||||
? 'border-green-500/30 text-green-400 bg-green-950/20'
|
||||
: 'border-yellow-500/30 text-yellow-400 bg-yellow-950/20'
|
||||
}`}
|
||||
>
|
||||
{m.transport === 'reticulum' ? 'DIRECT' : 'RELAY'}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[var(--text-secondary)] break-words whitespace-pre-wrap flex-1">
|
||||
{m.plaintext || '[encrypted]'}
|
||||
</span>
|
||||
<span className="text-[var(--text-muted)] shrink-0 text-[13px]">
|
||||
{timeAgo(m.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* Dead Drop chat UI: Infonet Terminal → Messages */}
|
||||
</div>
|
||||
|
||||
{/* INPUT BAR */}
|
||||
@@ -2341,21 +1570,15 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
? privateInfonetReady
|
||||
? `→ INFONET${selectedGate ? ` / ${selectedGate}` : ''}${privateInfonetTransportReady ? '' : ' / EXPERIMENTAL ENCRYPTION'}`
|
||||
: '→ PRIVATE LANE LOCKED'
|
||||
: activeTab === 'meshtastic'
|
||||
? canUsePublicMeshInput
|
||||
? meshDirectTarget
|
||||
? `→ MESH / TO ${meshDirectTarget.toUpperCase()} / FROM ${activePublicMeshAddress.toUpperCase()}`
|
||||
: `→ MESH / ${meshRegion} / ${meshChannel} / ${activePublicMeshAddress.toUpperCase()}`
|
||||
: publicMeshBlockedByWormhole
|
||||
? '→ MESH BLOCKED / WORMHOLE ACTIVE'
|
||||
: canUsePublicMeshInput
|
||||
? meshDirectTarget
|
||||
? `→ MESH / TO ${meshDirectTarget.toUpperCase()} / FROM ${activePublicMeshAddress.toUpperCase()}`
|
||||
: `→ MESH / ${meshRegion} / ${meshChannel} / ${activePublicMeshAddress.toUpperCase()}`
|
||||
: publicMeshBlockedByWormhole
|
||||
? '→ MESH BLOCKED / WORMHOLE ACTIVE'
|
||||
: hasStoredPublicLaneIdentity
|
||||
? '→ MESH OFF'
|
||||
: '→ MESH LOCKED'
|
||||
: activeTab === 'dms' && secureDmBlocked
|
||||
? '→ DEAD DROP LOCKED'
|
||||
: dmView === 'chat' && selectedContact
|
||||
? `→ DEAD DROP / ${selectedContact.slice(0, 14)}`
|
||||
: '→ SELECT TARGET'}
|
||||
: '→ MESH LOCKED'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -2386,19 +1609,6 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
OPEN PRIVATE LANE BRIEF
|
||||
</span>
|
||||
</button>
|
||||
) : activeTab === 'dms' && secureDmBlocked ? (
|
||||
<button
|
||||
onClick={() => setDeadDropUnlockOpen(true)}
|
||||
className="w-full flex items-center justify-between gap-2 px-3 py-2 border border-cyan-700/40 bg-cyan-950/15 text-cyan-300 hover:bg-cyan-950/25 hover:border-cyan-500/50 transition-colors"
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 text-sm font-mono tracking-[0.2em]">
|
||||
<Lock size={11} />
|
||||
UNLOCK DEAD DROP
|
||||
</span>
|
||||
<span className="text-[12px] font-mono text-cyan-300/70">
|
||||
NEED WORMHOLE
|
||||
</span>
|
||||
</button>
|
||||
) : activeTab === 'meshtastic' && !canUsePublicMeshInput ? (
|
||||
<button
|
||||
onClick={handleMeshActivationAction}
|
||||
|
||||
Reference in New Issue
Block a user