diff --git a/client/src/components/profile-page.riot b/client/src/components/profile-page.riot index 278efaf..5dffb1c 100644 --- a/client/src/components/profile-page.riot +++ b/client/src/components/profile-page.riot @@ -39,10 +39,16 @@ /> -
{state.error}
@@ -169,6 +175,11 @@ cursor: not-allowed; } + .input-mono { + font-family: var(--font-mono); + font-size: var(--text-xs); + } + .form-hint { display: block; margin-top: 4px; @@ -230,6 +241,13 @@ }) }, + npubDisplay() { + const hex = this.props.user?.nostr_pubkey + if (!hex) return '' + // Show truncated hex with npub prefix hint + return 'npub...' + hex.slice(-12) + }, + currentAvatar() { return getAvatarUrl(this.props.user, 96) }, diff --git a/client/src/services/avatar.js b/client/src/services/avatar.js index 78f5b5e..be735ff 100644 --- a/client/src/services/avatar.js +++ b/client/src/services/avatar.js @@ -11,7 +11,13 @@ * @returns {string} Avatar URL */ export function getAvatarUrl(user, size = 64) { - if (user?.avatar_url) return user.avatar_url + if (user?.avatar_url) { + // External URLs (e.g. Nostr profile pictures) are used as-is + if (user.avatar_url.startsWith('http://') || user.avatar_url.startsWith('https://')) { + return user.avatar_url + } + return user.avatar_url + } return avatarFromEmail(user?.email, size) } diff --git a/client/src/services/nostr.js b/client/src/services/nostr.js index b525513..3082053 100644 --- a/client/src/services/nostr.js +++ b/client/src/services/nostr.js @@ -14,62 +14,56 @@ export async function signEvent(event) { return window.nostr.signEvent(event) } +const RELAYS = [ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol', +] + /** - * Fetch a Nostr kind:0 profile from a relay via WebSocket. + * Fetch a Nostr kind:0 profile from relays via WebSocket. + * Races multiple relays, returns first result. * Returns { name, picture } or null on timeout/error. */ export function fetchNostrProfile(pubkeyHex, timeoutMs = 5000) { return new Promise((resolve) => { - const relay = 'wss://relay.damus.io' - let ws - let timer + let resolved = false + const connections = [] - const cleanup = () => { - clearTimeout(timer) - try { ws?.close() } catch {} + const done = (result) => { + if (resolved) return + resolved = true + connections.forEach(ws => { try { ws.close() } catch {} }) + resolve(result) } - timer = setTimeout(() => { - cleanup() - resolve(null) - }, timeoutMs) + setTimeout(() => done(null), timeoutMs) - try { - ws = new WebSocket(relay) - } catch { - cleanup() - resolve(null) - return - } - - ws.onopen = () => { - const subId = 'profile_' + Math.random().toString(36).slice(2, 8) - ws.send(JSON.stringify(['REQ', subId, { kinds: [0], authors: [pubkeyHex], limit: 1 }])) - } - - ws.onmessage = (msg) => { + for (const relay of RELAYS) { try { - const data = JSON.parse(msg.data) - if (data[0] === 'EVENT' && data[2]?.kind === 0) { - const profile = JSON.parse(data[2].content) - cleanup() - resolve({ - name: profile.name || profile.display_name || null, - picture: profile.picture || null, - }) - } else if (data[0] === 'EOSE') { - // End of stored events — no profile found - cleanup() - resolve(null) - } - } catch { - // ignore parse errors, wait for timeout - } - } + const ws = new WebSocket(relay) + connections.push(ws) - ws.onerror = () => { - cleanup() - resolve(null) + ws.onopen = () => { + const subId = 'p_' + Math.random().toString(36).slice(2, 8) + ws.send(JSON.stringify(['REQ', subId, { kinds: [0], authors: [pubkeyHex], limit: 1 }])) + } + + ws.onmessage = (msg) => { + try { + const data = JSON.parse(msg.data) + if (data[0] === 'EVENT' && data[2]?.kind === 0) { + const profile = JSON.parse(data[2].content) + done({ + name: profile.name || profile.display_name || null, + picture: profile.picture || null, + }) + } + } catch {} + } + + ws.onerror = () => {} + } catch {} } }) } diff --git a/client/src/services/websocket.js b/client/src/services/websocket.js index a9b6822..0a68302 100644 --- a/client/src/services/websocket.js +++ b/client/src/services/websocket.js @@ -78,6 +78,7 @@ class WebSocketManager { disconnect() { this.token = null this.subscribedRooms.clear() + this.listeners.clear() if (this.reconnectTimer) { clearTimeout(this.reconnectTimer) this.reconnectTimer = null diff --git a/server/src/handlers/auth.rs b/server/src/handlers/auth.rs index 18338c0..3189440 100644 --- a/server/src/handlers/auth.rs +++ b/server/src/handlers/auth.rs @@ -64,6 +64,7 @@ pub async fn register( email: models::public_email(&email), display_name, avatar_url: None, + nostr_pubkey: None, }, })) } @@ -103,6 +104,7 @@ pub async fn login( email: models::public_email(&email), display_name, avatar_url, + nostr_pubkey: None, }, })) } @@ -111,18 +113,20 @@ pub async fn me( auth: AuthUser, State(state): State