feat(nostr): optional display name in the presence event, asked at toggle-on
Turning discovery on prompts for a name; it rides the public announcement (clean_display_name both directions: single line, control-stripped, 32-char cap — it round-trips through untrusted relays). Blank lists as npub only; off/on keeps the stored name; sending an empty name clears it. Discovery lists show the name with the npub beneath. Own-npub display switches to middle-ellipsis so the comparable tail stays visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a2ff3502bd
commit
2786c0727f
@@ -884,14 +884,16 @@ class RPCClient {
|
||||
// `handshake.poll` queues inbound requests into the federation pending
|
||||
// inbox for manual approval (it does NOT auto-accept).
|
||||
|
||||
async nostrDiscoveryStatus(): Promise<{ enabled: boolean; npub?: string | null }> {
|
||||
async nostrDiscoveryStatus(): Promise<{ enabled: boolean; npub?: string | null; name?: string | null }> {
|
||||
return this.call({ method: 'nostr.discovery-status', params: {} })
|
||||
}
|
||||
|
||||
async nostrSetDiscovery(enabled: boolean): Promise<{ enabled: boolean }> {
|
||||
async nostrSetDiscovery(enabled: boolean, name?: string): Promise<{ enabled: boolean }> {
|
||||
// `name` omitted = backend keeps the stored display name; empty string
|
||||
// clears it. Only sent when the caller explicitly provides it.
|
||||
return this.call({
|
||||
method: 'nostr.set-discovery',
|
||||
params: { enabled },
|
||||
params: name === undefined ? { enabled } : { enabled, name },
|
||||
timeout: 30000,
|
||||
})
|
||||
}
|
||||
@@ -902,6 +904,7 @@ class RPCClient {
|
||||
nostr_npub: string
|
||||
did: string
|
||||
version: string
|
||||
name?: string | null
|
||||
}>
|
||||
}> {
|
||||
return this.call({ method: 'handshake.discover', params: {}, timeout: 30000 })
|
||||
|
||||
@@ -55,7 +55,10 @@
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs text-white/50 mb-1">{{ t('web5.yourNodeNpub') }}</p>
|
||||
<p class="text-xs font-mono text-white/80 truncate" :title="nodeNpub">{{ nodeNpub }}</p>
|
||||
<p v-if="nodeName" class="text-sm text-white/90 truncate mb-0.5">{{ nodeName }}</p>
|
||||
<!-- Middle-ellipsis, never CSS truncate: the tail is the part a
|
||||
human compares against another listing, so it must stay visible -->
|
||||
<p class="text-xs font-mono text-white/80 truncate" :title="nodeNpub">{{ midNpub(nodeNpub) }}</p>
|
||||
</div>
|
||||
<button @click="copyNpub" class="shrink-0 p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="Copy">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -83,7 +86,8 @@
|
||||
class="p-3 bg-white/5 rounded-lg border border-white/10 flex items-start justify-between gap-3"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm text-white truncate">{{ shortNpub(node.nostr_npub) }}</div>
|
||||
<div class="text-sm text-white truncate">{{ node.name || shortNpub(node.nostr_npub) }}</div>
|
||||
<div v-if="node.name" class="text-[11px] text-white/50 font-mono truncate">{{ shortNpub(node.nostr_npub) }}</div>
|
||||
<div class="text-[11px] text-white/40 font-mono truncate">{{ node.did }}</div>
|
||||
<div class="text-[10px] text-white/30 mt-1">version {{ node.version || '?' }}</div>
|
||||
</div>
|
||||
@@ -115,6 +119,32 @@
|
||||
@send="confirmPeerRequest"
|
||||
@cancel="requestModalTarget = null"
|
||||
/>
|
||||
|
||||
<!-- Name prompt on the way to discoverable: the announcement is public,
|
||||
so the name travels with it. Blank is fine — npub-only listing. -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="showNameModal" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="cancelNameModal">
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div class="glass-card p-6 max-w-md w-full relative z-10">
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Name your node</h3>
|
||||
<p class="text-sm text-white/60 mb-4">Other nodes will see this name next to your npub in their discovery list. It's public. Leave blank to list as npub only.</p>
|
||||
<input
|
||||
v-model="nameInput"
|
||||
type="text"
|
||||
maxlength="32"
|
||||
placeholder="e.g. Dorian's basement node"
|
||||
class="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder-white/30 focus:outline-none focus:border-orange-500/50 mb-4"
|
||||
@keyup.enter="confirmNameModal"
|
||||
/>
|
||||
<div class="flex gap-3">
|
||||
<button @click="cancelNameModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">Cancel</button>
|
||||
<button @click="confirmNameModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30">Turn on discovery</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -139,15 +169,21 @@ const emit = defineEmits<{
|
||||
|
||||
const nodeVisibility = ref<VisibilityLevel>('hidden')
|
||||
const nodeNpub = ref<string | null>(null)
|
||||
const nodeName = ref<string | null>(null)
|
||||
const visibilityLoading = ref(false)
|
||||
const settingVisibility = ref(false)
|
||||
const discoverEnabled = ref(false)
|
||||
// Name-prompt state: turning discovery ON routes through a small dialog so
|
||||
// the operator can (optionally) name the node before it announces itself.
|
||||
const showNameModal = ref(false)
|
||||
const nameInput = ref('')
|
||||
|
||||
interface DiscoverableNode {
|
||||
nostr_pubkey: string
|
||||
nostr_npub: string
|
||||
did: string
|
||||
version: string
|
||||
name?: string | null
|
||||
}
|
||||
|
||||
const discoveredNodes = ref<DiscoverableNode[]>([])
|
||||
@@ -155,6 +191,12 @@ const discovering = ref(false)
|
||||
const requestingPeer = ref<string | null>(null)
|
||||
const requestedPeers = ref(new Set<string>())
|
||||
|
||||
/** Own-npub display: keep the start and the FULL tail visible, ellipsis in
|
||||
* the middle. (shortNpub below stays as-is — it formats the discovered list.) */
|
||||
function midNpub(npub: string): string {
|
||||
return npub.length > 24 ? `${npub.slice(0, 12)}…${npub.slice(-10)}` : npub
|
||||
}
|
||||
|
||||
function shortNpub(npub: string): string {
|
||||
if (!npub) return 'unknown'
|
||||
return npub.length > 21 ? `${npub.slice(0, 12)}…${npub.slice(-6)}` : npub
|
||||
@@ -175,6 +217,7 @@ async function loadVisibility() {
|
||||
])
|
||||
discoverEnabled.value = !!disc.enabled
|
||||
nodeNpub.value = disc.npub || null
|
||||
nodeName.value = disc.name || null
|
||||
nodeVisibility.value = (vis?.visibility as VisibilityLevel) || 'hidden'
|
||||
if (discoverEnabled.value) void discoverNodes()
|
||||
} catch {
|
||||
@@ -186,10 +229,33 @@ async function loadVisibility() {
|
||||
|
||||
async function toggleDiscoverable(enabled: boolean) {
|
||||
if (settingVisibility.value) return
|
||||
if (enabled) {
|
||||
// Turning ON goes through the name dialog: the node is about to announce
|
||||
// itself publicly, and this is the natural moment to (optionally) name it.
|
||||
nameInput.value = nodeName.value || ''
|
||||
showNameModal.value = true
|
||||
return
|
||||
}
|
||||
await applyDiscovery(false)
|
||||
}
|
||||
|
||||
function cancelNameModal() {
|
||||
showNameModal.value = false
|
||||
// The switch never actually flipped server-side; snap the UI back.
|
||||
discoverEnabled.value = false
|
||||
}
|
||||
|
||||
async function confirmNameModal() {
|
||||
showNameModal.value = false
|
||||
// Send exactly what's in the box: text sets the name, blank clears it.
|
||||
await applyDiscovery(true, nameInput.value.trim())
|
||||
}
|
||||
|
||||
async function applyDiscovery(enabled: boolean, name?: string) {
|
||||
settingVisibility.value = true
|
||||
try {
|
||||
// Public means public: the switch drives nostr presence publishing.
|
||||
const res = await rpcClient.nostrSetDiscovery(enabled)
|
||||
const res = await rpcClient.nostrSetDiscovery(enabled, name)
|
||||
discoverEnabled.value = !!res.enabled
|
||||
// Keep the legacy visibility string in sync (cosmetic; best-effort).
|
||||
const level: VisibilityLevel = enabled ? 'public' : 'hidden'
|
||||
@@ -198,8 +264,17 @@ async function toggleDiscoverable(enabled: boolean) {
|
||||
.then(() => { nodeVisibility.value = level })
|
||||
.catch(() => {})
|
||||
emit('toast', enabled ? 'Node is now publicly discoverable' : 'Node hidden from discovery')
|
||||
if (enabled) void discoverNodes()
|
||||
else discoveredNodes.value = []
|
||||
if (enabled) {
|
||||
if (name !== undefined) nodeName.value = name || null
|
||||
// Re-read status so the npub/name shown reflect post-enable state
|
||||
// without a page reload.
|
||||
rpcClient.nostrDiscoveryStatus()
|
||||
.then((s) => { nodeNpub.value = s.npub || null; nodeName.value = s.name || null })
|
||||
.catch(() => {})
|
||||
void discoverNodes()
|
||||
} else {
|
||||
discoveredNodes.value = []
|
||||
}
|
||||
} catch {
|
||||
emit('toast', t('web5.failedToUpdateVisibility'))
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user