diff --git a/app.js b/app.js index 4b51de8..877ef26 100644 --- a/app.js +++ b/app.js @@ -138,6 +138,7 @@ async function init() { updateLoad(95); setupControls(); + initVisitorIdentity(); window.addEventListener('resize', onResize); debugOverlay = document.getElementById('debug-overlay'); @@ -1085,12 +1086,93 @@ function addChatMessage(type, text) { const container = document.getElementById('chat-messages'); const div = document.createElement('div'); div.className = `chat-msg chat-msg-${type}`; - const prefixes = { user: '[ALEXANDER]', timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' }; + let userPrefix = '[GUEST]'; + if (type === 'user' && visitorPubkey) { + const short = visitorPubkey.slice(0, 8) + '…' + visitorPubkey.slice(-4); + userPrefix = `[${short}]`; + } else if (type === 'user') { + userPrefix = '[GUEST]'; + } + const prefixes = { user: userPrefix, timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' }; div.innerHTML = `${prefixes[type] || '[???]'} ${text}`; container.appendChild(div); container.scrollTop = container.scrollHeight; } +// ═══ NIP-07 VISITOR IDENTITY ═══ +let visitorPubkey = null; +const VISITOR_STORAGE_KEY = 'nexus_visitor_pubkey'; + +function initVisitorIdentity() { + const cached = localStorage.getItem(VISITOR_STORAGE_KEY); + if (cached) { + visitorPubkey = cached; + updateVisitorUI(true); + addChatMessage('system', `Welcome back, ${abbrPubkey(cached)}. Identity recognized.`); + } + + // Show connect button only if NIP-07 extension detected and not already connected + if (window.nostr && !visitorPubkey) { + document.getElementById('nostr-connect-btn').style.display = 'block'; + } + + document.getElementById('nostr-connect-btn').addEventListener('click', connectNostrIdentity); + document.getElementById('nostr-disconnect-btn').addEventListener('click', disconnectNostrIdentity); +} + +async function connectNostrIdentity() { + if (!window.nostr) return; + const btn = document.getElementById('nostr-connect-btn'); + btn.textContent = '⚡ CONNECTING…'; + btn.disabled = true; + try { + const pubkey = await window.nostr.getPublicKey(); + if (!pubkey) throw new Error('No pubkey returned'); + visitorPubkey = pubkey; + localStorage.setItem(VISITOR_STORAGE_KEY, pubkey); + updateVisitorUI(true); + addChatMessage('system', `Identity linked: ${abbrPubkey(pubkey)}. Timmy remembers you.`); + } catch (e) { + btn.textContent = '⚡ CONNECT IDENTITY'; + btn.disabled = false; + addChatMessage('error', 'Identity connection failed. Extension declined or unavailable.'); + } +} + +function disconnectNostrIdentity() { + visitorPubkey = null; + localStorage.removeItem(VISITOR_STORAGE_KEY); + updateVisitorUI(false); + addChatMessage('system', 'Identity disconnected. Returning to guest mode.'); + if (window.nostr) { + document.getElementById('nostr-connect-btn').style.display = 'block'; + } +} + +function updateVisitorUI(connected) { + const indicator = document.getElementById('visitor-indicator'); + const nameEl = document.getElementById('visitor-name'); + const connectBtn = document.getElementById('nostr-connect-btn'); + const disconnectBtn = document.getElementById('nostr-disconnect-btn'); + + if (connected && visitorPubkey) { + indicator.className = 'visitor-indicator connected'; + nameEl.className = 'connected'; + nameEl.textContent = abbrPubkey(visitorPubkey); + connectBtn.style.display = 'none'; + disconnectBtn.style.display = 'block'; + } else { + indicator.className = 'visitor-indicator'; + nameEl.className = ''; + nameEl.textContent = 'GUEST'; + disconnectBtn.style.display = 'none'; + } +} + +function abbrPubkey(pubkey) { + return pubkey.slice(0, 8) + '…' + pubkey.slice(-4); +} + // ═══ PORTAL INTERACTION ═══ function checkPortalProximity() { if (portalOverlayActive) return; diff --git a/index.html b/index.html index dd4d42d..d1b0830 100644 --- a/index.html +++ b/index.html @@ -80,6 +80,17 @@
+ +
+
VISITOR
+
+ + GUEST +
+ + +
+
diff --git a/style.css b/style.css index 407b5c8..930e85f 100644 --- a/style.css +++ b/style.css @@ -625,6 +625,94 @@ canvas#nexus-canvas { color: var(--color-primary); } +/* Visitor Identity Panel */ +.visitor-identity-panel { + position: absolute; + bottom: 80px; + right: var(--space-3); + width: 200px; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + border-left: 2px solid var(--color-secondary); + padding: var(--space-2) var(--space-3); + font-size: var(--text-xs); + pointer-events: auto; +} +.visitor-identity-label { + font-family: var(--font-display); + color: var(--color-secondary); + letter-spacing: 0.1em; + margin-bottom: var(--space-1); + opacity: 0.8; + font-size: 10px; +} +.visitor-identity-status { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-2); +} +.visitor-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-text-muted); + flex-shrink: 0; +} +.visitor-indicator.connected { + background: var(--color-primary); + box-shadow: 0 0 6px rgba(74, 240, 192, 0.6); + animation: visitor-pulse 2s ease-in-out infinite; +} +@keyframes visitor-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +#visitor-name { + color: var(--color-text); + font-family: var(--font-body); + font-size: var(--text-xs); + word-break: break-all; +} +#visitor-name.connected { + color: var(--color-primary); + font-weight: 600; +} +.nostr-connect-btn { + width: 100%; + background: transparent; + border: 1px solid var(--color-secondary); + color: var(--color-secondary); + font-family: var(--font-body); + font-size: 10px; + padding: var(--space-1) var(--space-2); + cursor: pointer; + letter-spacing: 0.05em; + border-radius: 2px; + transition: background var(--transition-ui), color var(--transition-ui); +} +.nostr-connect-btn:hover { + background: var(--color-secondary); + color: var(--color-bg); +} +.nostr-disconnect-btn { + width: 100%; + background: transparent; + border: 1px solid var(--color-text-muted); + color: var(--color-text-muted); + font-family: var(--font-body); + font-size: 10px; + padding: var(--space-1) var(--space-2); + cursor: pointer; + letter-spacing: 0.05em; + border-radius: 2px; + transition: border-color var(--transition-ui), color var(--transition-ui); +} +.nostr-disconnect-btn:hover { + border-color: var(--color-danger); + color: var(--color-danger); +} + /* Mobile adjustments */ @media (max-width: 480px) { .chat-panel {