diff --git a/demo/chat.js b/demo/chat.js new file mode 100644 index 0000000..c06fa2d --- /dev/null +++ b/demo/chat.js @@ -0,0 +1,466 @@ + + import { + relayInit, + generatePrivateKey, + getPublicKey, + getEventHash, + signEvent, + nip19, + nip04 + } from 'nostr-tools'; + + // CONFIGURATION - Only need CS team's pubkey! + const CONFIG = { + relays: [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://nos.lol', + 'wss://relay.btcforplebs.com', + 'wss://relay.logemedia.com' + ], + csPubkey: 'PUBKEY_TO_RECEICE_MESSAGES' // Replace with actual pubkey + }; + + // State + let state = { + isOpen: false, + messages: [], + inputMessage: '', + myPrivKey: null, + myPubKey: null, + relays: [], + connected: false, + sessionId: null + }; + + // Generate or retrieve session key from localStorage + function getSessionKey() { + const stored = localStorage.getItem('nostr_chat_session'); + if (stored) { + try { + const session = JSON.parse(stored); + // Reuse if less than 24 hours old + if (Date.now() - session.created < 24 * 60 * 60 * 1000) { + return session.privKey; + } + } catch (e) {} + } + + // Generate new ephemeral key + const privKey = generatePrivateKey(); + localStorage.setItem('nostr_chat_session', JSON.stringify({ + privKey, + created: Date.now() + })); + return privKey; + } + + // Initialize + async function init() { + // Get or create session key + state.myPrivKey = getSessionKey(); + state.myPubKey = getPublicKey(state.myPrivKey); + state.sessionId = state.myPubKey.substring(0, 8); + + console.log('π Session Identity:', nip19.npubEncode(state.myPubKey)); + + // Connect to relays + 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`); + + // Subscribe to replies from CS team + subscribeToReplies(); + + // Load any previous messages from this session + loadPreviousMessages(); + + state.connected = true; + render(); + } + + function checkConnection() { + const connected = state.relays.some(r => r.status === 1); // 1 = connected + state.connected = connected; + render(); + } + + // Subscribe to DMs from CS team + function subscribeToReplies() { + const filters = [{ + kinds: [4], + '#p': [state.myPubKey], + authors: [CONFIG.csPubkey], + since: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours + }]; + + console.log('π Subscribing to CS team replies...'); + + state.relays.forEach(relay => { + const sub = relay.sub(filters); + + sub.on('event', (event) => { + handleIncomingMessage(event); + }); + + sub.on('eose', () => { + console.log(`β Subscribed: ${relay.url}`); + }); + }); + } + + // Load previous messages from localStorage + 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) {} + } + } + + // Save messages to localStorage + function saveMessages() { + localStorage.setItem(`nostr_chat_messages_${state.sessionId}`, JSON.stringify(state.messages)); + } + + // Handle incoming DM from CS team + async function handleIncomingMessage(event) { + try { + // Check for duplicates + if (state.messages.find(m => m.id === event.id)) { + return; + } + + console.log('π¨ Received message from CS team'); + + // Decrypt + 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); + + // Notification + if (!document.hasFocus()) { + document.title = 'π¬ New message!'; + setTimeout(() => { + document.title = 'Nostr Support Chat'; + }, 3000); + } + } catch (error) { + console.error('Error decrypting message:', error); + } + } + + // Send message to CS team + async function sendMessage() { + if (!state.inputMessage.trim()) return; + + const messageText = state.inputMessage; + + try { + console.log('π Encrypting and sending...'); + + // Encrypt + const encrypted = await nip04.encrypt( + state.myPrivKey, + CONFIG.csPubkey, + messageText + ); + + // Create event + 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); + + // Publish to all relays + 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`); + + // Add to local messages + const message = { + id: event.id, + text: messageText, + sender: 'user', + timestamp: new Date().toISOString() + }; + + addMessage('user', messageText, message); + state.inputMessage = ''; + render(); + + } 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('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 + }); + } + +// Render function with mobile responsiveness +function render() { + const container = document.getElementById('chat-widget-root'); + + if (!container) return; + + if (!state.isOpen) { + container.innerHTML = ` +
Start a conversation
+