#10b981

sxclij / スク 💪 1/23 1:24:02 💕
input: very very long think and output HTML - nostr client - need "publish note", "recv pictures(like instagram)", "sign with extention NIP-07", "debug tool" - auto connect relays "wss://relay.nostr.wirednet.jp", "wss://relay.yabu.me" - enrich err handling output: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Nostr Client - Instagram-like Dark</title> <style> :root { --bg: #121212; --surface: #1e1e1e; --primary: #7c3aed; --primary-hover: #6d28d9; --text: #e0e0e0; --text-muted: #9e9e9e; --border: #333333; --success: #10b981; --error: #ef4444; --font-family: 'Inter', system-ui, -apple-system, sans-serif; } body { font-family: var(--font-family); margin: 0; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; overflow-x: hidden; display: flex; flex-direction: column; } .wrapper { display: flex; flex-grow: 1; } .left-panel { width: 300px; background: var(--surface); border-right: 1px solid var(--border); padding: 1.5rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1.25rem; box-sizing: border-box; } .right-panel { flex: 1; overflow-y: auto; padding: 1.5rem; box-sizing: border-box; } .container { max-width: 100%; margin: 0 auto; padding: 0; } .status-indicator { padding: 0.6rem 1.1rem; border-radius: 8px; display: inline-flex; align-items: center; gap: 0.6rem; font-weight: 500; border: 1px solid var(--border); background: var(--surface); font-size: 0.9rem; margin-bottom: 0.5rem; } .status-indicator::before { content: ''; width: 9px; height: 9px; border-radius: 50%; background: currentColor; display: block; } .connected { color: var(--success); } .disconnected { color: var(--error); } .relay-control { display: flex; flex-direction: column; gap: 0.75rem; margin: 1rem 0; } .relay-buttons { display: flex; gap: 0.75rem; } input, textarea { background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 0.8rem; border-radius: 8px; transition: border-color 0.2s, box-shadow 0.2s; width: 100%; font-family: var(--font-family); font-size: 1rem; box-sizing: border-box; } input:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.3); } .note { background: var(--surface); padding: 1rem; border-radius: 12px; margin-bottom: 1rem; border: 1px solid var(--border); transition: transform 0.15s ease-out, box-shadow 0.15s ease-out; box-shadow: 0 2px 5px rgba(0,0,0,0.1); display: flex; flex-direction: column; } .note:hover { transform: translateY(-2px); box-shadow: 0 5px 10px rgba(0,0,0,0.15); } .note-header { display: flex; align-items: center; margin-bottom: 0.75rem; } .note-avatar-placeholder { width: 34px; height: 34px; border-radius: 50%; background-color: var(--border); margin-right: 0.75rem; } .note-user-info { display: flex; flex-direction: column; } .pubkey-username { font-weight: 500; font-size: 0.95rem; color: var(--text); margin-bottom: 0.1rem; } .timestamp { font-style: normal; opacity: 0.7; font-size: 0.8rem; } .note-content { white-space: pre-wrap; color: var(--text); font-size: 1rem; margin-bottom: 1rem; } .note-actions { display: flex; justify-content: flex-start; gap: 1.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.75rem; } .note-action-button { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1.1rem; opacity: 0.8; transition: opacity 0.2s; } .note-action-button:hover { opacity: 1; color: var(--text); } button { background: var(--primary); color: white; border: none; padding: 0.8rem 1.6rem; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background-color 0.2s ease-in-out, transform 0.15s ease-out, box-shadow 0.15s ease-out; display: inline-flex; align-items: center; justify-content: center; gap: 0.6rem; font-size: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } button:hover { background: var(--primary-hover); transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } button:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.05); } button:disabled { background: var(--text-muted); cursor: not-allowed; opacity: 0.7; transform: none; box-shadow: none; } .auth-options { display: grid; gap: 0.75rem; margin-top: 1rem; } .char-count { text-align: right; font-size: 0.85rem; color: var(--text-muted); margin: 0.4rem 0; } #notes { display: grid; gap: 1.5rem; } #relay-status-container { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.3rem; } .relay-status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: var(--text-muted); } .relay-status-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background-color: var(--error); } .relay-status-dot.connected { background-color: var(--success); } @media (max-width: 768px) { .wrapper { flex-direction: column; } .left-panel { width: 100%; border-right: none; border-bottom: 1px solid var(--border); overflow-y: visible; } .right-panel { padding-top: 0; } } </style> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> </head> <body> <div class="wrapper"> <aside class="left-panel"> <div class="container"> <header> <h1>Nostr Client</h1> </header> <div id="connection-status" class="status-indicator disconnected"> Status: Disconnected </div> <div class="relay-control"> <input type="url" id="relayUrls" value="wss://yabu.me,wss://relay.damus.io,wss://nostr.oxtr.dev" placeholder="wss://relay1.url,wss://relay2.url" pattern="^wss?://.+(,wss?://.+)*"> <div class="relay-buttons"> <button id="connectRelaysButton">Connect Relays</button> <button id="disconnectRelaysButton">Disconnect Relays</button> </div> <div id="relay-status-container"> <!-- Relay statuses will be injected here --> </div> </div> <section class="publish-section"> <h3>Publish Note</h3> <textarea id="noteContent" placeholder="Share something..." maxlength="500"></textarea> <div class="char-count"> <span id="charCount">0</span>/500 characters </div> <div class="auth-options"> <input type="password" id="privateKey" placeholder="nsec or hex key (temporary)"> <button id="generateKeyButton">Generate Key</button> <button id="publishButton">Publish Securely</button> <button id="nip07Button">Sign with Extension (NIP-07)</button> </div> </section> </div> </aside> <main class="right-panel"> <div class="container"> <div id="notes"></div> </div> </main> </div> <script src="https://unpkg.com/nostr-tools@1.13.0/lib/nostr.bundle.js"></script> <script> let relays = {}; let relayStatuses = {}; let activeSubscriptions = {}; let isConnected = false; const elements = { status: document.getElementById('connection-status'), publishButton: document.getElementById('publishButton'), nip07Button: document.getElementById('nip07Button'), noteContent: document.getElementById('noteContent'), privateKey: document.getElementById('privateKey'), notes: document.getElementById('notes'), charCount: document.getElementById('charCount'), relayUrlsInput: document.getElementById('relayUrls'), generateKeyButton: document.getElementById('generateKeyButton'), connectRelaysButton: document.getElementById('connectRelaysButton'), disconnectRelaysButton: document.getElementById('disconnectRelaysButton'), relayStatusContainer: document.getElementById('relay-status-container') }; elements.publishButton.addEventListener('click', publishNote); elements.nip07Button.addEventListener('click', publishWithNIP07); elements.noteContent.addEventListener('input', updateCharCount); elements.generateKeyButton.addEventListener('click', generateKey); elements.connectRelaysButton.addEventListener('click', connectToRelays); elements.disconnectRelaysButton.addEventListener('click', disconnectFromRelays); checkNIP07Support(); updateCharCount(); initializeRelayUrls(); renderRelayStatusesUI(); function updateCharCount() { elements.charCount.textContent = elements.noteContent.value.length; } function setStatus(connected) { isConnected = connected; elements.status.textContent = `Status: ${connected ? 'Connected' : 'Disconnected'}`; elements.status.className = `status-indicator ${connected ? 'connected' : 'disconnected'}`; } function updateRelayStatusUI(relayUrl, connected) { relayStatuses[relayUrl] = connected; renderRelayStatusesUI(); } function renderRelayStatusesUI() { elements.relayStatusContainer.innerHTML = ''; let overallConnected = false; for (const relayUrl in relayStatuses) { const connected = relayStatuses[relayUrl]; if (connected) overallConnected = true; const statusDiv = document.createElement('div'); statusDiv.className = 'relay-status'; statusDiv.innerHTML = ` <div class="relay-status-dot ${connected ? 'connected' : ''}"></div> <span>${relayUrl}</span> `; elements.relayStatusContainer.appendChild(statusDiv); } setStatus(overallConnected); } async function connectToRelays() { const relayUrlsInput = elements.relayUrlsInput.value.trim(); const urls = relayUrlsInput.split(',').map(url => url.trim()).filter(url => url); if (urls.length === 0) { alert('Please enter at least one relay URL.'); return; } disconnectFromRelays(); relays = {}; relayStatuses = {}; for (const relayUrl of urls) { if (!relayUrl.startsWith('wss://') && !relayUrl.startsWith('ws://')) { console.warn(`Invalid WebSocket URL: ${relayUrl}`); updateRelayStatusUI(relayUrl, false); continue; } connectToRelay(relayUrl); } saveRelayUrls(relayUrlsInput); } async function connectToRelay(relayUrl) { if (relays[relayUrl] && relays[relayUrl].readyState === WebSocket.OPEN) { return; } const socket = new WebSocket(relayUrl); relays[relayUrl] = socket; relayStatuses[relayUrl] = false; socket.onopen = () => { console.log(`Connected to relay: ${relayUrl}`); updateRelayStatusUI(relayUrl, true); subscribeToNotes(relayUrl, socket); }; socket.onmessage = (e) => { try { const [type, subId, event] = JSON.parse(e.data); if (type === 'EVENT' && event?.kind === 1) { displayNote(event); } } catch (error) { console.warn(`Error parsing message from ${relayUrl}:`, error); } }; socket.onerror = (error) => { console.error(`WebSocket error for ${relayUrl}:`, error); updateRelayStatusUI(relayUrl, false); if (relays[relayUrl]) { relays[relayUrl].close(); delete relays[relayUrl]; } }; socket.onclose = () => { console.log(`Disconnected from relay: ${relayUrl}`); updateRelayStatusUI(relayUrl, false); if (relays[relayUrl]) { delete relays[relayUrl]; } if (activeSubscriptions[relayUrl]) { delete activeSubscriptions[relayUrl]; } }; } function disconnectFromRelays() { for (const relayUrl in relays) { if (relays[relayUrl] && relays[relayUrl].readyState === WebSocket.OPEN) { console.log(`Disconnecting from relay: ${relayUrl}`); relays[relayUrl].close(); } delete relays[relayUrl]; updateRelayStatusUI(relayUrl, false); } relays = {}; activeSubscriptions = {}; setStatus(false); } function subscribeToNotes(relayUrl, socket) { if (!socket || socket.readyState !== WebSocket.OPEN) { console.warn(`Socket for ${relayUrl} is not open, cannot subscribe.`); return; } const subId = `sub-${Date.now()}-${relayUrl.replace(/[^a-zA-Z0-9]/g, '')}`; activeSubscriptions[relayUrl] = subId; const filter = { kinds: [1], limit: 50, since: Math.floor(Date.now() / 1000) - 86400 }; socket.send(JSON.stringify(['REQ', subId, filter])); } function displayNote(event) { const noteElement = document.createElement('div'); noteElement.className = 'note'; const noteHeader = document.createElement('div'); noteHeader.className = 'note-header'; const avatarPlaceholder = document.createElement('div'); avatarPlaceholder.className = 'note-avatar-placeholder'; noteHeader.appendChild(avatarPlaceholder); const userInfo = document.createElement('div'); userInfo.className = 'note-user-info'; const usernameElement = document.createElement('span'); usernameElement.className = 'pubkey-username'; usernameElement.textContent = NostrTools.nip19.npubEncode(event.pubkey).slice(0, 12) + '...'; userInfo.appendChild(usernameElement); const timestampElement = document.createElement('span'); timestampElement.className = 'timestamp'; const date = new Date(event.created_at * 1000).toLocaleString(); timestampElement.textContent = date; userInfo.appendChild(timestampElement); noteHeader.appendChild(userInfo); noteElement.appendChild(noteHeader); const safeContent = event.content.replace(/</g, '<').replace(/>/g, '>'); const contentElement = document.createElement('div'); contentElement.className = 'note-content'; contentElement.innerHTML = safeContent; noteElement.appendChild(contentElement); const noteActions = document.createElement('div'); noteActions.className = 'note-actions'; const likeButton = document.createElement('button'); likeButton.className = 'note-action-button'; likeButton.innerHTML = '❤️'; likeButton.title = 'Like (Placeholder)'; noteActions.appendChild(likeButton); noteElement.appendChild(noteActions); elements.notes.prepend(noteElement); } async function publishNote() { const content = elements.noteContent.value.trim(); const privateKey = elements.privateKey.value.trim(); if (!content) return alert('Please enter some content'); if (!privateKey) return alert('Please provide a private key'); if (Object.keys(relays).length === 0) return alert('Not connected to any relay. Please connect to relays.'); try { const sk = privateKey.startsWith('nsec1') ? NostrTools.nip19.decode(privateKey).data : privateKey; const event = createBaseEvent(content, NostrTools.getPublicKey(sk)); event.sig = await NostrTools.signEvent(event, sk); sendEvent(event); elements.noteContent.value = ''; updateCharCount(); } catch (error) { alert(`Publishing failed: ${error.message}`); } } function createBaseEvent(content, pubkey) { return { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content, pubkey, id: '', sig: '' }; } async function publishWithNIP07() { if (!window.nostr) { alert('No NIP-07 extension detected'); return; } if (Object.keys(relays).length === 0) return alert('Not connected to any relay. Please connect to relays.'); const content = elements.noteContent.value.trim(); if (!content) return alert('Please enter some content'); elements.nip07Button.textContent = 'Signing...'; elements.nip07Button.disabled = true; try { const pubkey = await window.nostr.getPublicKey(); const baseEvent = createBaseEvent(content, pubkey); const signedEvent = await window.nostr.signEvent(baseEvent); sendEvent(signedEvent); elements.noteContent.value = ''; updateCharCount(); } catch (error) { alert(`Extension signing failed: ${error.message}`); } finally { elements.nip07Button.textContent = 'Sign with Extension (NIP-07)'; elements.nip07Button.disabled = false; } } function sendEvent(event) { for (const relayUrl in relays) { const socket = relays[relayUrl]; if (socket && socket.readyState === WebSocket.OPEN) { if (!event.id) { event.id = NostrTools.getEventHash(event); } socket.send(JSON.stringify(['EVENT', event])); } } } function checkNIP07Support() { if (!window.nostr) { elements.nip07Button.disabled = true; elements.nip07Button.title = "NIP-07 extension not detected"; } } function saveRelayUrls(urls) { localStorage.setItem('relayUrls', urls); } function initializeRelayUrls() { const savedRelayUrls = localStorage.getItem('relayUrls'); if (savedRelayUrls) { elements.relayUrlsInput.value = savedRelayUrls; connectToRelays(); } } function generateKey() { const sk = NostrTools.generatePrivateKey(); const nsec = NostrTools.nip19.nsecEncode(sk); elements.privateKey.value = nsec; } </script> </body> </html>
NIP-07
💕
Send kind:7