Create DID button generates a did:key identity (tries backend RPC first, falls back to client-side Web Crypto P-256 key generation). DID stored in localStorage. Copy DID button for sharing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
874 lines
42 KiB
Vue
874 lines
42 KiB
Vue
<template>
|
|
<div>
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-white mb-2">Web5</h1>
|
|
<p class="text-white/70">Decentralized identity and data protocols</p>
|
|
<p class="text-sm text-white/60 mt-2">Earn networking profits by hosting decentralized services</p>
|
|
</div>
|
|
|
|
<!-- Quick Actions Container -->
|
|
<div class="glass-card p-6 mb-6">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
|
|
<!-- Networking Profits -->
|
|
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="relative shrink-0">
|
|
<span class="text-2xl text-orange-500 font-bold">₿</span>
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium text-white">Networking Profits</p>
|
|
<p class="text-xs text-orange-500 font-medium">₿0.024</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DID Status -->
|
|
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="relative shrink-0">
|
|
<div class="w-3 h-3 rounded-full" :class="didStatus === 'active' ? 'bg-green-400' : 'bg-yellow-400'"></div>
|
|
<div v-if="didStatus === 'active'" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-white">DID Status</p>
|
|
<p v-if="userDid" class="text-xs text-white/60 font-mono truncate" :title="userDid">{{ userDid }}</p>
|
|
<p v-else class="text-xs text-white/60 capitalize">{{ didStatus }}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
v-if="userDid"
|
|
@click="copyDid"
|
|
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
|
>
|
|
{{ didCopied ? 'Copied!' : 'Copy DID' }}
|
|
</button>
|
|
<button
|
|
v-else
|
|
@click="createDID"
|
|
:disabled="creatingDid"
|
|
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
{{ creatingDid ? 'Creating...' : 'Create DID' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Wallet Connection -->
|
|
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="relative shrink-0">
|
|
<div class="w-3 h-3 rounded-full" :class="walletConnected ? 'bg-green-400' : 'bg-red-400'"></div>
|
|
<div v-if="walletConnected" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium text-white">Wallet</p>
|
|
<p class="text-xs text-white/60">{{ walletConnected ? 'Connected' : 'Disconnected' }}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="connectWallet"
|
|
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
:disabled="connectingWallet"
|
|
>
|
|
{{ connectingWallet ? 'Connecting...' : walletConnected ? 'Disconnect' : 'Connect' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Nostr Relay Status -->
|
|
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="relative shrink-0">
|
|
<div class="w-3 h-3 rounded-full" :class="nostrRelaysConnected > 0 ? 'bg-green-400' : 'bg-red-400'"></div>
|
|
<div v-if="nostrRelaysConnected > 0" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
|
</div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium text-white">Nostr Relays</p>
|
|
<p class="text-xs text-white/60">{{ nostrRelaysConnected }} connected</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="manageRelays"
|
|
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
|
>
|
|
Manage
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Connected Nodes -->
|
|
<div data-controller-container tabindex="0" class="flex flex-col gap-3 p-4 bg-white/5 rounded-lg min-w-0">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="relative shrink-0">
|
|
<div class="w-3 h-3 rounded-full" :class="connectedNodesCount > 0 ? 'bg-green-400' : 'bg-amber-400'"></div>
|
|
<div v-if="connectedNodesCount > 0" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-pulse opacity-75"></div>
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-white">Connected Nodes</p>
|
|
<p class="text-xs text-white/60">{{ connectedNodesCount }} peer{{ connectedNodesCount !== 1 ? 's' : '' }} known</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="showSendMessageModal = true"
|
|
class="w-fit px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors"
|
|
>
|
|
Send Message
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Send Message Modal -->
|
|
<Teleport to="body">
|
|
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="closeSendMessageModal()">
|
|
<div ref="sendMessageModalRef" class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
|
<h3 class="text-lg font-semibold text-white mb-4">Send Message (over Tor)</h3>
|
|
<p class="text-white/70 text-sm mb-4">Messages are sent over the Tor network to the selected peer.</p>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/80 mb-2">To</label>
|
|
<select
|
|
v-model="sendMessageTo"
|
|
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
|
>
|
|
<option value="">Select a peer...</option>
|
|
<option v-for="p in peers" :key="p.pubkey" :value="p.onion">
|
|
{{ p.name || p.onion || p.pubkey.slice(0, 12) + '...' }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/80 mb-2">Message</label>
|
|
<textarea
|
|
v-model="sendMessageText"
|
|
rows="3"
|
|
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
|
placeholder="Type your message..."
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3 mt-6">
|
|
<button
|
|
@click="sendMessage"
|
|
:disabled="!sendMessageTo || !sendMessageText.trim() || sendingMessage"
|
|
class="flex-1 px-4 py-2 rounded-lg bg-orange-500 text-white font-medium hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{{ sendingMessage ? 'Sending...' : 'Send' }}
|
|
</button>
|
|
<button
|
|
@click="closeSendMessageModal()"
|
|
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
<p v-if="sendMessageError" class="mt-3 text-sm text-red-400">{{ sendMessageError }}</p>
|
|
<p v-if="sendMessageSuccess" class="mt-3 text-sm text-green-400">{{ sendMessageSuccess }}</p>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Core Services Overview Cards -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
<!-- Bitcoin Domain Name Portfolio -->
|
|
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
|
|
<div class="flex items-start gap-4 mb-4 shrink-0">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Bitcoin Domain Names</h2>
|
|
<p class="text-white/70 text-sm mb-4">Manage your .btc domain portfolio</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 flex-1 min-h-0">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Domains Owned</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">5 domains</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Registration Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Active</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Expiring Soon</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">1 domain</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
|
Manage Domains
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Web5 Wallet -->
|
|
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
|
|
<div class="flex items-start gap-4 mb-4 shrink-0">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Web5 Wallet</h2>
|
|
<p class="text-white/70 text-sm mb-4">Decentralized wallet for Web5 assets</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 flex-1 min-h-0">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-lg text-orange-500 font-bold">₿</span>
|
|
<span class="text-white/80 text-sm">Balance</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm text-orange-500">₿0.025</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Wallet Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Connected</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Transactions</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">12 pending</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
|
Open Wallet
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Nostr Relays -->
|
|
<div data-controller-container tabindex="0" class="glass-card p-6 flex flex-col h-full min-h-0">
|
|
<div class="flex items-start gap-4 mb-4 shrink-0">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Nostr Relays</h2>
|
|
<p class="text-white/70 text-sm mb-4">Decentralized social networking relays</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 flex-1 min-h-0">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Relays Connected</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">8 active</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Relay Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Syncing</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Events Stored</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">1.2M events</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-auto pt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors shrink-0">
|
|
Manage Relays
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Connected Nodes (P2P over Tor) -->
|
|
<div ref="nodesContainerRef" data-controller-container tabindex="0" class="glass-card p-6 lg:col-span-3 scroll-mt-24">
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Connected Nodes</h2>
|
|
<p class="text-white/70 text-sm mb-4">Peer nodes discovered via Nostr. Messages sent over Tor.</p>
|
|
</div>
|
|
<button
|
|
@click="loadPeers"
|
|
class="px-3 py-1.5 glass-button glass-button-sm rounded text-xs font-medium text-white/90 hover:text-white transition-colors shrink-0"
|
|
>
|
|
{{ loadingPeers ? '...' : 'Refresh' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabs: Peers | Messages -->
|
|
<div class="flex gap-1 mb-4 border-b border-white/10">
|
|
<button
|
|
@click="nodesContainerTab = 'peers'"
|
|
class="px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
|
|
:class="nodesContainerTab === 'peers' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white/80 hover:bg-white/5'"
|
|
>
|
|
Peers
|
|
<span v-if="peers.length > 0" class="ml-1.5 text-xs text-white/50">({{ peers.length }})</span>
|
|
</button>
|
|
<button
|
|
@click="switchToMessagesTab"
|
|
class="px-4 py-2 text-sm font-medium rounded-t-lg transition-colors flex items-center gap-1.5"
|
|
:class="nodesContainerTab === 'messages' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white/80 hover:bg-white/5'"
|
|
>
|
|
Messages
|
|
<span v-if="receivedMessages.length > 0" class="ml-1.5 text-xs" :class="unreadCount > 0 ? 'text-orange-400' : 'text-white/50'">({{ receivedMessages.length }})</span>
|
|
<span v-if="unreadCount > 0" class="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Peers tab -->
|
|
<div v-show="nodesContainerTab === 'peers'" class="space-y-2 max-h-48 overflow-y-auto">
|
|
<div v-if="peers.length === 0" class="p-4 text-center text-white/60 text-sm">
|
|
No peers yet. Add a peer manually or use Discover to find nodes on Nostr.
|
|
</div>
|
|
<div
|
|
v-for="p in peers"
|
|
:key="p.pubkey"
|
|
class="flex items-center justify-between p-3 bg-white/5 rounded-lg"
|
|
>
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="w-2 h-2 rounded-full shrink-0" :class="peerReachable[p.onion] ? 'bg-green-400' : 'bg-amber-400'"></div>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-mono text-white/90 truncate">{{ p.name || p.onion || p.pubkey.slice(0, 16) + '...' }}</p>
|
|
<p class="text-xs text-white/50 truncate">{{ p.onion }}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="showSendMessageModal = true; sendMessageTo = p.onion"
|
|
class="px-2 py-1 text-xs rounded bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 transition-colors shrink-0"
|
|
>
|
|
Message
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages tab -->
|
|
<div v-show="nodesContainerTab === 'messages'" class="space-y-2 max-h-64 overflow-y-auto">
|
|
<div v-if="loadingMessages" class="p-4 text-center text-white/60 text-sm">
|
|
Loading messages...
|
|
</div>
|
|
<div v-else-if="receivedMessages.length === 0" class="p-4 text-center text-white/60 text-sm">
|
|
No messages yet. Messages from peers will appear here.
|
|
</div>
|
|
<div
|
|
v-for="(m, idx) in receivedMessages"
|
|
:key="idx"
|
|
class="p-3 bg-white/5 rounded-lg border-l-2 border-orange-500/50"
|
|
>
|
|
<div class="flex items-center justify-between gap-2 mb-1">
|
|
<p class="text-xs font-mono text-white/60 truncate" :title="m.from_pubkey">{{ m.from_pubkey.slice(0, 16) }}...</p>
|
|
<span class="text-xs text-white/40 shrink-0">{{ formatMessageTime(m.timestamp) }}</span>
|
|
</div>
|
|
<p class="text-sm text-white/90 break-words">{{ m.message }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
v-if="nodesContainerTab === 'peers'"
|
|
@click="discoverAndAddPeers"
|
|
:disabled="discovering"
|
|
class="mt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
{{ discovering ? 'Discovering...' : 'Discover Nodes on Nostr' }}
|
|
</button>
|
|
<button
|
|
v-else
|
|
@click="loadReceivedMessages"
|
|
:disabled="loadingMessages"
|
|
class="mt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
{{ loadingMessages ? 'Loading...' : 'Refresh Messages' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Protocol Overview Cards -->
|
|
<div v-if="false" class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
<!-- Decentralized Identifiers (DIDs) -->
|
|
<div class="glass-card p-6">
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Decentralized Identifiers</h2>
|
|
<p class="text-white/70 text-sm mb-4">Self-owned identifiers for verifiable digital identity</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Active DIDs</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">3 created</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Verification Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Verified</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Key Management</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">Secure</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors">
|
|
Manage DIDs
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Decentralized Web Nodes (DWNs) -->
|
|
<div class="glass-card p-6">
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Decentralized Web Nodes</h2>
|
|
<p class="text-white/70 text-sm mb-4">Personal data stores you control</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Data Stores</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">2 active</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Storage Used</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">1.2 GB</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Sync Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Synced</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors">
|
|
Manage DWNs
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Self-Sovereign Identity Service -->
|
|
<div class="glass-card p-6">
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">Self-Sovereign Identity</h2>
|
|
<p class="text-white/70 text-sm mb-4">Create and manage decentralized identities</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Identities</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">2 managed</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Service Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Running</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Credentials</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">5 issued</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors">
|
|
Manage SSI Service
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Self-Sovereign Identity SDK -->
|
|
<div class="glass-card p-6">
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<h2 class="text-xl font-semibold text-white mb-2">SSI SDK</h2>
|
|
<p class="text-white/70 text-sm mb-4">Developer toolkit for Web5 applications</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">SDK Version</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">v1.2.0</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">Active Apps</span>
|
|
</div>
|
|
<span class="text-white/60 text-sm">4 integrated</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
<span class="text-white/80 text-sm">API Status</span>
|
|
</div>
|
|
<span class="text-green-400 text-sm font-medium">Available</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="mt-4 w-full px-4 py-2 glass-button rounded-lg text-sm font-medium text-white/90 hover:text-white transition-colors">
|
|
View SDK Documentation
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
import { useMessageToast } from '@/composables/useMessageToast'
|
|
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
|
|
|
const route = useRoute()
|
|
const messageToast = useMessageToast()
|
|
|
|
const storedDid = ref<string | null>(null)
|
|
try {
|
|
storedDid.value = localStorage.getItem('neode_did') || null
|
|
} catch { /* noop */ }
|
|
|
|
const userDid = computed(() => storedDid.value)
|
|
|
|
const didStatus = computed<'active' | 'inactive' | 'pending'>(() =>
|
|
userDid.value ? 'active' : 'inactive'
|
|
)
|
|
|
|
const creatingDid = ref(false)
|
|
const didCopied = ref(false)
|
|
|
|
async function createDID() {
|
|
creatingDid.value = true
|
|
try {
|
|
// Try backend RPC first
|
|
const res = await rpcClient.call<{ did: string }>({ method: 'identity.create-did' })
|
|
storedDid.value = res.did
|
|
localStorage.setItem('neode_did', res.did)
|
|
} catch {
|
|
// Fallback: generate a did:key locally using Web Crypto
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
const exported = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
|
const bytes = new Uint8Array(exported)
|
|
// Multicodec prefix for P-256 public key (0x1200) + base58btc
|
|
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
const did = `did:key:z${hex}`
|
|
storedDid.value = did
|
|
localStorage.setItem('neode_did', did)
|
|
} finally {
|
|
creatingDid.value = false
|
|
}
|
|
}
|
|
|
|
async function copyDid() {
|
|
if (!userDid.value) return
|
|
await navigator.clipboard.writeText(userDid.value)
|
|
didCopied.value = true
|
|
setTimeout(() => { didCopied.value = false }, 2000)
|
|
}
|
|
|
|
// DWN Sync Status: 'synced' | 'syncing' | 'error'
|
|
const dwnSyncStatus = ref<'synced' | 'syncing' | 'error'>('synced')
|
|
const syncingDWNs = ref(false)
|
|
|
|
// Wallet Connection
|
|
const walletConnected = ref(true)
|
|
const connectingWallet = ref(false)
|
|
|
|
// Nostr Relays
|
|
const nostrRelaysConnected = ref(8)
|
|
|
|
// Connected Nodes (peers)
|
|
const peers = ref<Array<{ onion: string; pubkey: string; name?: string }>>([])
|
|
const loadingPeers = ref(false)
|
|
const peerReachable = ref<Record<string, boolean>>({})
|
|
const connectedNodesCount = computed(() => peers.value.length)
|
|
|
|
// Send Message modal
|
|
const showSendMessageModal = ref(false)
|
|
const sendMessageModalRef = ref<HTMLElement | null>(null)
|
|
const sendMessageRestoreFocusRef = ref<HTMLElement | null>(null)
|
|
function closeSendMessageModal() {
|
|
sendMessageRestoreFocusRef.value?.focus?.()
|
|
showSendMessageModal.value = false
|
|
}
|
|
useModalKeyboard(sendMessageModalRef, showSendMessageModal, closeSendMessageModal, { restoreFocusRef: sendMessageRestoreFocusRef })
|
|
const sendMessageTo = ref('')
|
|
const sendMessageText = ref('')
|
|
const sendingMessage = ref(false)
|
|
const sendMessageError = ref('')
|
|
const sendMessageSuccess = ref('')
|
|
const discovering = ref(false)
|
|
|
|
// Connected Nodes container: tabs + messages (uses shared composable for polling from Dashboard)
|
|
const nodesContainerRef = ref<HTMLElement | null>(null)
|
|
const nodesContainerTab = ref<'peers' | 'messages'>('peers')
|
|
const { receivedMessages, loadingMessages, unreadCount, loadReceivedMessages, markAsRead } = messageToast
|
|
|
|
function formatMessageTime(ts: string): string {
|
|
try {
|
|
const d = new Date(ts)
|
|
const now = new Date()
|
|
const diff = now.getTime() - d.getTime()
|
|
if (diff < 60000) return 'Just now'
|
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
|
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
|
|
return d.toLocaleDateString()
|
|
} catch {
|
|
return ts
|
|
}
|
|
}
|
|
|
|
function switchToMessagesTab() {
|
|
nodesContainerTab.value = 'messages'
|
|
markAsRead()
|
|
}
|
|
|
|
async function loadPeers() {
|
|
loadingPeers.value = true
|
|
try {
|
|
const res = await rpcClient.listPeers()
|
|
peers.value = res.peers || []
|
|
for (const p of peers.value) {
|
|
try {
|
|
const check = await rpcClient.checkPeerReachable(p.onion)
|
|
peerReachable.value[p.onion] = check.reachable
|
|
} catch {
|
|
peerReachable.value[p.onion] = false
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load peers:', e)
|
|
} finally {
|
|
loadingPeers.value = false
|
|
}
|
|
}
|
|
|
|
async function sendMessage() {
|
|
if (!sendMessageTo.value || !sendMessageText.value.trim()) return
|
|
sendingMessage.value = true
|
|
sendMessageError.value = ''
|
|
sendMessageSuccess.value = ''
|
|
try {
|
|
await rpcClient.sendMessageToPeer(sendMessageTo.value, sendMessageText.value.trim())
|
|
sendMessageSuccess.value = 'Message sent over Tor!'
|
|
sendMessageText.value = ''
|
|
setTimeout(() => {
|
|
showSendMessageModal.value = false
|
|
sendMessageSuccess.value = ''
|
|
}, 1500)
|
|
} catch (e) {
|
|
sendMessageError.value = e instanceof Error ? e.message : 'Failed to send'
|
|
} finally {
|
|
sendingMessage.value = false
|
|
}
|
|
}
|
|
|
|
async function discoverAndAddPeers() {
|
|
discovering.value = true
|
|
try {
|
|
const res = await rpcClient.discoverNodes()
|
|
const nodes = res.nodes || []
|
|
for (const n of nodes) {
|
|
if (n.onion && n.pubkey) {
|
|
try {
|
|
await rpcClient.addPeer({ onion: n.onion, pubkey: n.pubkey })
|
|
} catch {
|
|
// may already exist
|
|
}
|
|
}
|
|
}
|
|
await loadPeers()
|
|
} catch (e) {
|
|
console.error('Discover failed:', e)
|
|
} finally {
|
|
discovering.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadPeers()
|
|
loadReceivedMessages()
|
|
// Open Messages tab when navigated via toast (e.g. ?tab=messages)
|
|
if (route.query.tab === 'messages') {
|
|
nodesContainerTab.value = 'messages'
|
|
markAsRead()
|
|
nextTick(() => {
|
|
nodesContainerRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
})
|
|
}
|
|
})
|
|
|
|
watch(() => route.query.tab, (tab) => {
|
|
if (tab === 'messages') {
|
|
nodesContainerTab.value = 'messages'
|
|
markAsRead()
|
|
nextTick(() => {
|
|
nodesContainerRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
})
|
|
}
|
|
})
|
|
|
|
|
|
// @ts-ignore - Function kept for future use
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
function _syncDWNs() {
|
|
syncingDWNs.value = true
|
|
dwnSyncStatus.value = 'syncing'
|
|
// TODO: Implement DWN sync API call
|
|
console.log('Syncing DWNs...')
|
|
setTimeout(() => {
|
|
syncingDWNs.value = false
|
|
dwnSyncStatus.value = 'synced'
|
|
}, 2000)
|
|
}
|
|
|
|
function connectWallet() {
|
|
if (walletConnected.value) {
|
|
walletConnected.value = false
|
|
// TODO: Implement wallet disconnect API call
|
|
console.log('Disconnecting wallet...')
|
|
} else {
|
|
connectingWallet.value = true
|
|
// TODO: Implement wallet connect API call
|
|
console.log('Connecting wallet...')
|
|
setTimeout(() => {
|
|
connectingWallet.value = false
|
|
walletConnected.value = true
|
|
}, 2000)
|
|
}
|
|
}
|
|
|
|
function manageRelays() {
|
|
// TODO: Navigate to relay management or open modal
|
|
console.log('Managing Nostr relays...')
|
|
}
|
|
</script>
|
|
|
|
|