frontend: polish app launch and release experience

This commit is contained in:
archipelago
2026-06-11 00:24:40 -04:00
parent c393b96da3
commit 1a3d726eac
140 changed files with 5930 additions and 920 deletions
@@ -67,6 +67,13 @@
</div>
<div v-else class="space-y-2">
<div v-if="loading && nodes.length > 0" class="p-2 text-center text-white/45 text-xs flex items-center justify-center gap-2">
<svg class="animate-spin h-3.5 w-3.5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Searching relays...
</div>
<div
v-for="node in nodes"
:key="node.nostr_pubkey"
@@ -189,4 +196,6 @@ function shortNpub(npub: string): string {
if (!npub || npub.length < 16) return npub
return `${npub.slice(0, 14)}${npub.slice(-8)}`
}
defineExpose({ refresh })
</script>
@@ -1,5 +1,5 @@
<template>
<div v-if="node" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/10 backdrop-blur-md" @click.self="handleClose">
<div v-if="node" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="handleClose">
<div class="glass-card p-6 w-full max-w-lg max-h-[80vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-white">Node Details</h2>
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="glass-card p-6 max-h-[60vh] flex flex-col">
<h2 class="text-lg font-semibold text-white mb-4">Your Nodes <span v-if="trustedNodes.length > 0" class="text-sm font-normal text-white/50">({{ trustedNodes.length }})</span></h2>
<div v-if="loading" class="flex items-center gap-3 py-8 justify-center">
<div v-if="loading && nodes.length === 0" class="flex items-center gap-3 py-8 justify-center">
<div class="w-5 h-5 border-2 border-white/20 border-t-orange-400 rounded-full animate-spin"></div>
<span class="text-white/60 text-sm">Loading nodes...</span>
</div>
@@ -0,0 +1,69 @@
import { flushPromises, mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import DiscoverModal from '../DiscoverModal.vue'
import { rpcClient } from '@/api/rpc-client'
vi.mock('@/api/rpc-client', () => ({
rpcClient: {
handshakeDiscover: vi.fn(),
handshakeConnect: vi.fn(),
},
}))
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
function makeNode() {
return {
nostr_pubkey: 'pubkey-one',
nostr_npub: 'npub1abcdefghijklmnopqrstuvwxyz',
did: 'did:key:node',
version: '1.8-alpha',
}
}
describe('DiscoverModal', () => {
it('keeps discoverable nodes visible while relay refresh is pending or fails', async () => {
vi.mocked(rpcClient.handshakeDiscover).mockResolvedValueOnce({ nodes: [makeNode()] })
const wrapper = mount(DiscoverModal, {
props: { visible: false, outboundSent: [] },
global: {
stubs: {
Teleport: true,
},
},
})
await wrapper.setProps({ visible: true })
await flushPromises()
expect(wrapper.text()).toContain('did:key:node')
expect(wrapper.text()).toContain('version 1.8-alpha')
const pending = deferred<{ nodes: [] }>()
vi.mocked(rpcClient.handshakeDiscover).mockReturnValueOnce(pending.promise)
const refresh = (wrapper.vm as unknown as { refresh: () => Promise<void> }).refresh()
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('did:key:node')
expect(wrapper.text()).toContain('Searching relays...')
expect(wrapper.text()).not.toContain('No discoverable nodes found')
pending.reject(new Error('relay offline'))
await refresh
await flushPromises()
expect(wrapper.text()).toContain('did:key:node')
expect(wrapper.text()).toContain('relay offline')
expect(wrapper.text()).not.toContain('Searching relays...')
expect(wrapper.text()).not.toContain('No discoverable nodes found')
})
})
@@ -0,0 +1,32 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import NodeList from '../NodeList.vue'
import type { FederatedNode } from '../types'
const trustedNode: FederatedNode = {
did: 'did:key:z6MkTrustedNode',
pubkey: 'trusted-pubkey',
onion: 'trusted.onion',
trust_level: 'trusted',
added_at: '2026-06-10T00:00:00Z',
name: 'Trusted Node',
last_seen: '2026-06-10T00:00:00Z',
}
describe('NodeList', () => {
it('keeps existing nodes visible during background refresh loading', () => {
const wrapper = mount(NodeList, {
props: {
nodes: [trustedNode],
loading: true,
error: '',
syncResults: [],
dwnSyncDotClass: 'bg-green-400',
cleaningNodes: false,
},
})
expect(wrapper.text()).toContain('Trusted Node')
expect(wrapper.text()).not.toContain('Loading nodes...')
})
})