From 5fe040ff28b596abfbb30c67c06faf0bc67b56bf Mon Sep 17 00:00:00 2001 From: BTCforPlebs Date: Mon, 6 Oct 2025 16:20:34 -0400 Subject: [PATCH] 0.0.1 --- demo/chat.js | 466 ++++++++++++++++++++++++++++++++++++++++++++++++ demo/index.html | 8 +- readme.md | 24 +-- 3 files changed, 485 insertions(+), 13 deletions(-) create mode 100644 demo/chat.js 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 = ` +
+ +
+ `; + return; + } + + // Full chat box rendering with mobile responsiveness + container.innerHTML = ` +
+
+ +
+
+
+

Instant Chat

+
+
+ + ${state.connected ? `P2P E2EE β€’ ${state.relays.length} relays` : 'Connecting...'} + +
+
+ +
+
+ + +
+ ${state.messages.length === 0 ? ` +
+ + + +

Start a conversation

+
+ ` : state.messages.map(msg => { + if (msg.sender === 'system') { + return ` +
+
+ ${escapeHtml(msg.text)} +
+
+ `; + } else if (msg.sender === 'user') { + return ` +
+
+
+ ${escapeHtml(msg.text)} +
+
${formatTime(msg.timestamp)}
+
+
+ `; + } else if (msg.sender === 'cs') { + return ` +
+
+
+
Loge Media Team
+ ${escapeHtml(msg.text)} +
+
${formatTime(msg.timestamp)}
+
+
+ `; + } + return ''; + }).join('')} +
+ + +
+
+ + +
+
+
+
+ `; + + const messageInput = document.getElementById('message-input'); + if (messageInput) { + messageInput.addEventListener('input', (e) => { + state.inputMessage = e.target.value; + const sendButton = document.querySelector('button[onclick="window.sendMessage()"]'); + if (sendButton) { + sendButton.disabled = !state.connected || !e.target.value.trim(); + } + }); + messageInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }); + + // Auto-scroll to bottom on mobile + const messagesContainer = document.getElementById('messages'); + if (messagesContainer && state.messages.length > 0) { + setTimeout(() => { + messagesContainer.scrollTop = messagesContainer.scrollHeight; + }, 100); + } + + messageInput.focus(); + } +} + +// Global functions +window.openChat = async () => { + state.isOpen = true; + render(); + if (state.relays.length === 0) { + await init(); + } +}; + +window.closeChat = () => { + state.isOpen = false; + render(); +}; + +window.sendMessage = sendMessage; + +// Initial render +render(); diff --git a/demo/index.html b/demo/index.html index f8f0fb6..aaa10ed 100644 --- a/demo/index.html +++ b/demo/index.html @@ -98,7 +98,7 @@

Ready to get started?

- @@ -117,11 +117,13 @@ - + + + + -
\ No newline at end of file diff --git a/readme.md b/readme.md index 1c0fe87..bd170f1 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ This repository contains a lightweight demo that shows how to embed a Nostr chat 1. **Clone the repository** ```bash - git clone https://github.com/your-username/nostr-web-chat-plugin.git + git clone https://github.com/btcforplebs/nostr-web-chat-plugin.git ``` 2. **Start the demo server** (works on macOS, Linux, Windows – NodeΒ 18+ is required) @@ -15,20 +15,24 @@ This repository contains a lightweight demo that shows how to embed a Nostr chat npm install npm run dev ``` - The server will start at [http://localhost:5173](http://localhost:5173). + The server will start the demo at [http://localhost:4000](http://localhost:4000/demo). -3. **Copy the widget snippet** – add this to the place in your HTML where you want the chat box to appear: + ## Website Embed (Node.js) + +1. **copy chat.JS to nodeJS project** + + /src/chat.js move to website /src folder + +2. **Copy the widget snippet** – add this to the place in your HTML where you want the chat box to appear: ```html -
- - + + + + +
``` - - `#nostr-chat` is the DOM element that will contain the widget. - - Replace `relay` with your preferred Nostr relay if you wish. 4. **Open your page** in any browser. The chat widget should load automatically and allow users to send/receive messages.