[claude] NIP-07 visitor identity in the workshop (#12) #20
75
app.js
75
app.js
@@ -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();
|
||||
|
||||
17
index.html
17
index.html
@@ -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 <span>Mouse</span> look <span>Enter</span> chat
|
||||
|
||||
75
style.css
75
style.css
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user