/** * Nostr Chat Widget - Embeddable Version (Glassmorphism Design) * * EMBED IT WITH: * */ (function() { 'use strict'; // Get configuration from script tag const scriptTag = document.currentScript; const csPubkey = scriptTag.getAttribute('data-nostr-pubkey') || 'PUBKEY_TO_RECEICE_MESSAGES'; const brandName = scriptTag.getAttribute('data-brand-name') || 'Support Team Messaging'; const primaryColor = scriptTag.getAttribute('data-color') || '#fdad01'; const secondaryColor = scriptTag.getAttribute('data-color-secondary') || '#000000'; // Default relay configuration const DEFAULT_RELAYS = [ 'wss://relay.damus.io', 'wss://relay.primal.net', 'wss://nos.lol', 'wss://relay.btcforplebs.com' ]; // Add viewport meta tag for mobile optimization let viewportMeta = document.querySelector('meta[name="viewport"]'); if (!viewportMeta) { viewportMeta = document.createElement('meta'); viewportMeta.name = 'viewport'; viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; document.head.appendChild(viewportMeta); } // Inject custom styles with glassmorphism const style = document.createElement('style'); style.textContent = ` .safe-area-bottom { padding-bottom: env(safe-area-inset-bottom); } #nostr-chat-widget-root > div { pointer-events: auto !important; z-index: 99999 !important; } .glass-morphism { background: rgba(255, 255, 255, 0.08); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.15); } .glass-morphism-light { background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } .glass-input { background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.3); } .glass-input:focus { background: rgba(255, 255, 255, 0.25); border: 1px solid rgba(255, 255, 255, 0.4); } .glass-input::placeholder { color: rgba(255, 255, 255, 0.6); } @media (max-width: 640px) { #nostr-chat-widget-root .chat-window-mobile { position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; width: 100% !important; height: 100% !important; max-height: 100vh !important; border-radius: 0 !important; } } `; document.head.appendChild(style); // Create widget container const widgetRoot = document.createElement('div'); widgetRoot.id = 'nostr-chat-widget-root'; document.body.appendChild(widgetRoot); // Import map for nostr-tools const importMap = document.createElement('script'); importMap.type = 'importmap'; importMap.textContent = JSON.stringify({ imports: { 'nostr-tools': 'https://esm.sh/nostr-tools@1.17.0' } }); document.head.appendChild(importMap); // Main widget script const widgetScript = document.createElement('script'); widgetScript.type = 'module'; widgetScript.textContent = ` import { relayInit, generatePrivateKey, getPublicKey, getEventHash, signEvent, nip19, nip04 } from 'nostr-tools'; const CONFIG = { relays: ${JSON.stringify(DEFAULT_RELAYS)}, csPubkey: '${csPubkey}', brandName: '${brandName}', primaryColor: '${primaryColor}', secondaryColor: '${secondaryColor}' }; let state = { isOpen: false, messages: [], inputMessage: '', myPrivKey: null, myPubKey: null, relays: [], connected: false, sessionId: null }; function getSessionKey() { const stored = localStorage.getItem('nostr_chat_session'); if (stored) { try { const session = JSON.parse(stored); if (Date.now() - session.created < 24 * 60 * 60 * 1000) { return session.privKey; } } catch (e) {} } const privKey = generatePrivateKey(); localStorage.setItem('nostr_chat_session', JSON.stringify({ privKey, created: Date.now() })); return privKey; } async function init() { state.myPrivKey = getSessionKey(); state.myPubKey = getPublicKey(state.myPrivKey); state.sessionId = state.myPubKey.substring(0, 8); console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey)); const relayPromises = CONFIG.relays.map(async (url) => { try { const relay = relayInit(url); relay.on('connect', () => { console.log(\`✓ Connected to \${url}\`); checkConnection(); }); relay.on('disconnect', () => { console.log(\`✗ Disconnected from \${url}\`); }); await relay.connect(); return relay; } catch (error) { console.error(\`Failed: \${url}:\`, error); return null; } }); state.relays = (await Promise.all(relayPromises)).filter(r => r !== null); if (state.relays.length === 0) { addMessage('system', '⚠️ Failed to connect to any relays'); return; } console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`); subscribeToReplies(); loadPreviousMessages(); state.connected = true; render(); } function checkConnection() { const connected = state.relays.some(r => r.status === 1); state.connected = connected; render(); } function subscribeToReplies() { const filters = [{ kinds: [4], '#p': [state.myPubKey], authors: [CONFIG.csPubkey], since: Math.floor(Date.now() / 1000) - 86400 }]; console.log('🔔 Subscribing to replies...'); state.relays.forEach(relay => { const sub = relay.sub(filters); sub.on('event', (event) => { handleIncomingMessage(event); }); sub.on('eose', () => { console.log(\`✓ Subscribed: \${relay.url}\`); }); }); } function loadPreviousMessages() { const stored = localStorage.getItem(\`nostr_chat_messages_\${state.sessionId}\`); if (stored) { try { const messages = JSON.parse(stored); messages.forEach(msg => state.messages.push(msg)); render(); } catch (e) {} } } function saveMessages() { localStorage.setItem(\`nostr_chat_messages_\${state.sessionId}\`, JSON.stringify(state.messages)); } async function handleIncomingMessage(event) { try { if (state.messages.find(m => m.id === event.id)) { return; } console.log('📨 Received message'); const decryptedText = await nip04.decrypt( state.myPrivKey, event.pubkey, event.content ); const message = { id: event.id, text: decryptedText, sender: 'cs', timestamp: new Date(event.created_at * 1000).toISOString() }; addMessage('cs', decryptedText, message); if (!document.hasFocus()) { const originalTitle = document.title; document.title = '💬 New message!'; setTimeout(() => { document.title = originalTitle; }, 3000); } } catch (error) { console.error('Error decrypting message:', error); } } async function sendMessage() { if (!state.inputMessage.trim()) return; const messageText = state.inputMessage; state.inputMessage = ''; // Show message immediately (optimistic UI) const tempMessage = { id: 'temp_' + Date.now(), text: messageText, sender: 'user', timestamp: new Date().toISOString() }; state.messages.push(tempMessage); render(); scrollToBottom(); try { console.log('🔐 Encrypting and sending...'); const encrypted = await nip04.encrypt( state.myPrivKey, CONFIG.csPubkey, messageText ); let event = { kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [['p', CONFIG.csPubkey]], content: encrypted, pubkey: state.myPubKey }; event.id = getEventHash(event); event.sig = signEvent(event, state.myPrivKey); let published = 0; for (const relay of state.relays) { try { await relay.publish(event); published++; console.log(\`✓ Published to \${relay.url}\`); } catch (err) { console.error(\`✗ Failed: \${relay.url}:\`, err); } } if (published === 0) { addMessage('system', '⚠️ Failed to send - no relay connections'); return; } console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`); // Update temp message with real ID const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id); if (msgIndex !== -1) { state.messages[msgIndex].id = event.id; } saveMessages(); } catch (error) { console.error('Error sending:', error); addMessage('system', '⚠️ Failed to send message'); } } function addMessage(sender, text, fullMessage = null) { const msg = fullMessage || { id: Date.now().toString(), text, sender, timestamp: new Date().toISOString() }; state.messages.push(msg); saveMessages(); render(); scrollToBottom(); } function scrollToBottom() { setTimeout(() => { const container = document.getElementById('nostr-messages'); if (container) { container.scrollTop = container.scrollHeight; } }, 100); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatTime(timestamp) { const date = new Date(timestamp); return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); } function render() { const container = document.getElementById('nostr-chat-widget-root'); if (!container) return; if (!state.isOpen) { container.innerHTML = \`
Start a conversation