[claude] NIP-07 visitor identity in the workshop (#12) #20

Closed
claude wants to merge 1 commits from claude:claude/issue-12 into main
3 changed files with 165 additions and 2 deletions

75
app.js
View File

@@ -34,6 +34,9 @@ let debugOverlay;
let frameCount = 0, lastFPSTime = 0, fps = 0;
let chatOpen = true;
let loadProgress = 0;
let nostrPubkey = null;
const NOSTR_STORAGE_KEY = 'nexus_nostr_pubkey';
// ═══ INIT ═══
function init() {
@@ -95,6 +98,9 @@ function init() {
setupControls();
window.addEventListener('resize', onResize);
// NIP-07 identity (after DOM is ready)
initNostrIdentity();
// Debug overlay ref
debugOverlay = document.getElementById('debug-overlay');
@@ -866,7 +872,8 @@ 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]' };
const userPrefix = nostrPubkey ? `[${formatPubkey(nostrPubkey)}]` : '[VISITOR]';
const prefixes = { user: userPrefix, timmy: '[TIMMY]', system: '[NEXUS]', error: '[ERROR]' };
div.innerHTML = `<span class="chat-msg-prefix">${prefixes[type] || '[???]'}</span> ${text}`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
@@ -980,5 +987,71 @@ function onResize() {
composer.setSize(w, h);
}
// ═══ NIP-07 NOSTR IDENTITY ═══
function formatPubkey(hex) {
return hex.slice(0, 8) + '…' + hex.slice(-4);
}
function setNostrIdentity(pubkey) {
nostrPubkey = pubkey;
localStorage.setItem(NOSTR_STORAGE_KEY, pubkey);
document.getElementById('nostr-anon').style.display = 'none';
document.getElementById('nostr-connect-btn').style.display = 'none';
document.getElementById('nostr-pubkey-display').textContent = formatPubkey(pubkey);
document.getElementById('nostr-connected').style.display = 'flex';
addChatMessage('system', `Identity connected: <span style="color:var(--color-primary)">${formatPubkey(pubkey)}</span>. Timmy recognizes you.`);
}
function clearNostrIdentity() {
nostrPubkey = null;
localStorage.removeItem(NOSTR_STORAGE_KEY);
document.getElementById('nostr-connected').style.display = 'none';
document.getElementById('nostr-anon').style.display = 'flex';
if (window.nostr) {
document.getElementById('nostr-connect-btn').style.display = 'flex';
}
addChatMessage('system', 'Identity disconnected. Visitor is anonymous.');
}
function initNostrIdentity() {
const connectBtn = document.getElementById('nostr-connect-btn');
const disconnectBtn = document.getElementById('nostr-disconnect-btn');
connectBtn.addEventListener('click', async () => {
try {
connectBtn.disabled = true;
connectBtn.textContent = 'Connecting…';
const pubkey = await window.nostr.getPublicKey();
if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey)) {
setNostrIdentity(pubkey);
} else {
throw new Error('Invalid pubkey returned');
}
} catch (err) {
addChatMessage('error', 'Identity request declined or extension unavailable.');
connectBtn.disabled = false;
connectBtn.innerHTML = '<span class="nostr-icon">⚡</span> Connect Identity';
}
});
disconnectBtn.addEventListener('click', () => {
clearNostrIdentity();
});
// Check for saved identity in localStorage
const saved = localStorage.getItem(NOSTR_STORAGE_KEY);
if (saved && /^[0-9a-f]{64}$/i.test(saved)) {
nostrPubkey = saved;
document.getElementById('nostr-anon').style.display = 'none';
document.getElementById('nostr-pubkey-display').textContent = formatPubkey(saved);
document.getElementById('nostr-connected').style.display = 'flex';
} else if (window.nostr) {
// Extension available, offer connect
document.getElementById('nostr-anon').style.display = 'none';
connectBtn.style.display = 'flex';
}
// else: stays anonymous (default)
}
// ═══ START ═══
init();

View File

@@ -86,7 +86,7 @@
<span class="chat-msg-prefix">[NEXUS]</span> Sovereign space initialized. Timmy is observing.
</div>
<div class="chat-msg chat-msg-timmy">
<span class="chat-msg-prefix">[TIMMY]</span> Welcome to the Nexus, Alexander. All systems nominal.
<span class="chat-msg-prefix">[TIMMY]</span> Welcome to the Nexus. Connect your Nostr identity or explore anonymously.
</div>
</div>
<div class="chat-input-row">
@@ -95,6 +95,21 @@
</div>
</div>
<!-- Top Right: Nostr Identity -->
<div id="nostr-identity" class="nostr-identity">
<button id="nostr-connect-btn" class="nostr-btn nostr-btn-connect" style="display:none;" title="Connect Nostr identity via NIP-07">
<span class="nostr-icon"></span> Connect Identity
</button>
<div id="nostr-connected" class="nostr-connected" style="display:none;">
<span class="nostr-icon"></span>
<span id="nostr-pubkey-display" class="nostr-pubkey"></span>
<button id="nostr-disconnect-btn" class="nostr-btn nostr-btn-disconnect" title="Disconnect identity"></button>
</div>
<div id="nostr-anon" class="nostr-anon">
<span class="nostr-icon-muted"></span> Anonymous
</div>
</div>
<!-- Minimap / Controls hint -->
<div class="hud-controls">
<span>WASD</span> move &nbsp; <span>Mouse</span> look &nbsp; <span>Enter</span> chat

View File

@@ -330,6 +330,81 @@ canvas#nexus-canvas {
background: rgba(74, 240, 192, 0.1);
}
/* === NOSTR IDENTITY === */
.nostr-identity {
position: absolute;
top: var(--space-3);
right: var(--space-4);
display: flex;
align-items: center;
gap: var(--space-2);
pointer-events: auto;
}
.nostr-btn {
background: none;
border: 1px solid var(--color-border);
border-radius: 4px;
font-family: var(--font-body);
font-size: var(--text-xs);
cursor: pointer;
transition: background var(--transition-ui), border-color var(--transition-ui);
display: flex;
align-items: center;
gap: var(--space-1);
}
.nostr-btn-connect {
padding: var(--space-1) var(--space-3);
color: var(--color-primary);
border-color: rgba(74, 240, 192, 0.3);
}
.nostr-btn-connect:hover {
background: rgba(74, 240, 192, 0.1);
border-color: rgba(74, 240, 192, 0.6);
}
.nostr-btn-disconnect {
padding: 2px var(--space-2);
color: var(--color-text-muted);
border-color: transparent;
font-size: 10px;
line-height: 1;
}
.nostr-btn-disconnect:hover {
color: var(--color-danger);
border-color: rgba(255, 68, 102, 0.3);
}
.nostr-connected {
display: flex;
align-items: center;
gap: var(--space-2);
background: rgba(74, 240, 192, 0.05);
border: 1px solid rgba(74, 240, 192, 0.25);
border-radius: 4px;
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
}
.nostr-icon {
color: var(--color-primary);
font-style: normal;
}
.nostr-pubkey {
font-family: var(--font-body);
font-size: var(--text-xs);
color: var(--color-primary);
letter-spacing: 0.05em;
}
.nostr-anon {
font-size: var(--text-xs);
color: var(--color-text-muted);
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
}
.nostr-icon-muted {
color: var(--color-text-muted);
font-style: normal;
}
/* === FOOTER === */
.nexus-footer {
position: fixed;