From 4f3403f34ea5913ea1c69fc769297a4eee167764 Mon Sep 17 00:00:00 2001 From: BTCforPlebs Date: Mon, 6 Oct 2025 11:32:37 -0400 Subject: [PATCH] Initial commit: Nostr chat widget --- .gitignore | 36 ++++ chat.css | 226 +++++++++++++++++++++ config_docs.md | 343 +++++++++++++++++++++++++++++++ demo/index.html | 146 ++++++++++++++ license | 21 ++ nostr_chat_readme.md | 169 ++++++++++++++++ package_json.json | 40 ++++ src/chat.js | 466 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1447 insertions(+) create mode 100644 .gitignore create mode 100644 chat.css create mode 100644 config_docs.md create mode 100644 demo/index.html create mode 100644 license create mode 100644 nostr_chat_readme.md create mode 100644 package_json.json create mode 100644 src/chat.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a3570e --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock + +# Environment variables +.env +.env.local +.env.production + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* + +# Build +dist/ +build/ + +# Testing +coverage/ + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/chat.css b/chat.css new file mode 100644 index 0000000..77b2fc0 --- /dev/null +++ b/chat.css @@ -0,0 +1,226 @@ +/* Nostr Chat Widget Styles */ + +/* Widget positioning and z-index management */ +#chat-widget-root > div { + pointer-events: auto !important; + position: fixed !important; + bottom: 1.5rem !important; + right: 1.5rem !important; + z-index: 99999 !important; +} + +/* Chat bubble button */ +#chat-bubble { + position: fixed !important; + bottom: 24px !important; + right: 24px !important; + width: 60px; + height: 60px; + background: #fdad01; + border: 2px solid #fdad01; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: transform 0.2s; + z-index: 999999 !important; +} + +#chat-bubble:hover { + transform: scale(1.1); +} + +#chat-bubble.hidden { + display: none !important; +} + +/* Chat window container */ +#chat-window { + position: fixed !important; + bottom: 24px !important; + right: 24px !important; + width: 380px; + height: 600px; + background: white; + border: 2px solid #fdad01; + border-radius: 12px; + display: none; + flex-direction: column; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + z-index: 999999 !important; +} + +#chat-window.open { + display: flex !important; +} + +/* Chat header */ +#chat-header { + background: linear-gradient(to right, #fdad01, #ff9500); + color: white; + padding: 16px; + border-radius: 10px 10px 0 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +#status { + font-size: 12px; + opacity: 0.9; + margin-top: 4px; +} + +#close-btn { + background: rgba(255, 255, 255, 0.2); + border: none; + color: white; + width: 32px; + height: 32px; + border-radius: 6px; + cursor: pointer; + font-size: 18px; +} + +/* Messages container */ +#messages { + flex: 1; + overflow-y: auto; + padding: 16px; + background: #f9fafb; +} + +/* Message bubbles */ +.message { + margin-bottom: 12px; + display: flex; +} + +.message.user { + justify-content: flex-end; +} + +.message.cs, +.message.system { + justify-content: flex-start; +} + +.message-bubble { + max-width: 75%; + padding: 10px 14px; + border-radius: 12px; + font-size: 14px; +} + +.message.user .message-bubble { + background: #fdad01; + color: white; +} + +.message.cs .message-bubble { + background: white; + border: 1px solid #e5e7eb; + color: #1f2937; +} + +.message.system .message-bubble { + background: #dbeafe; + color: #1e40af; + font-size: 13px; +} + +.message-time { + font-size: 11px; + opacity: 0.7; + margin-top: 4px; +} + +/* Input area */ +#chat-input { + padding: 16px; + border-top: 1px solid #e5e7eb; + background: white; + border-radius: 0 0 10px 10px; +} + +#input-container { + display: flex; + gap: 8px; +} + +#message-input { + flex: 1; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 8px; + font-size: 14px; + outline: none; +} + +#message-input:focus { + border-color: #fdad01; +} + +#send-btn { + background: #fdad01; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: 600; +} + +#send-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Connecting animation */ +.connecting { + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* Mobile responsive styles */ +@media (max-width: 600px) { + #chat-window { + bottom: 0 !important; + right: 0 !important; + width: 100vw !important; + height: 100vh !important; + border-radius: 0 !important; + max-width: 100% !important; + } + + #chat-header { + border-radius: 0 !important; + } + + #chat-input { + border-radius: 0 0 0 0 !important; + } + + #chat-bubble { + bottom: 16px !important; + right: 16px !important; + width: 50px; + height: 50px; + } +} + +/* Safe area for mobile devices with notches */ +.safe-area-bottom { + padding-bottom: env(safe-area-inset-bottom); +} \ No newline at end of file diff --git a/config_docs.md b/config_docs.md new file mode 100644 index 0000000..567e1cb --- /dev/null +++ b/config_docs.md @@ -0,0 +1,343 @@ +# Configuration Guide + +This guide walks you through configuring the Nostr Chat Widget for your website. + +## Table of Contents +- [Basic Setup](#basic-setup) +- [Getting Your Nostr Keys](#getting-your-nostr-keys) +- [Relay Configuration](#relay-configuration) +- [Customization](#customization) +- [Receiving Messages](#receiving-messages) + +## Basic Setup + +### Step 1: Include Dependencies + +Add these to your HTML ``: + +```html + + + + + +``` + +### Step 2: Add the Widget + +Before your closing `` tag: + +```html + + + + +
+``` + +### Step 3: Configure Your Public Key + +Open `chat.js` and locate the `CONFIG` object: + +```javascript +const CONFIG = { + relays: [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://nos.lol', + 'wss://relay.btcforplebs.com', + 'wss://relay.logemedia.com' + ], + csPubkey: 'YOUR_PUBLIC_KEY_HERE' // Replace with your hex public key +}; +``` + +## Getting Your Nostr Keys + +### Option 1: Using an Existing Nostr Client + +If you already use Nostr: + +1. Open your Nostr client (Damus, Amethyst, Snort, etc.) +2. Go to Settings → Keys/Security +3. Copy your **public key** (NOT your private key) +4. If it starts with `npub1`, convert it to hex format (see below) + +### Option 2: Generate New Keys + +For a fresh customer support identity: + +1. Install a Nostr client: + - Desktop: [Nostr.band](https://nostr.band), [Nostrudel](https://nostrudel.ninja) + - Mobile: [Damus (iOS)](https://damus.io), [Amethyst (Android)](https://github.com/vitorpamplona/amethyst) + +2. Create a new account +3. **IMPORTANT**: Securely save your private key (nsec) +4. Copy your public key for the config + +### Converting npub to Hex + +If your public key starts with `npub1`: + +**Online Tool:** +- Visit [nostr.band/tools](https://nostr.band/tools) +- Paste your npub +- Copy the hex version + +**Using JavaScript:** +```javascript +import { nip19 } from 'nostr-tools'; +const hex = nip19.decode('npub1...').data; +console.log(hex); +``` + +## Relay Configuration + +### Default Relays + +The widget connects to multiple relays for reliability: + +```javascript +relays: [ + 'wss://relay.damus.io', // General purpose, reliable + 'wss://relay.primal.net', // Popular, good uptime + 'wss://nos.lol', // Community favorite + 'wss://relay.btcforplebs.com', // Bitcoin-focused + 'wss://relay.logemedia.com' // Custom relay +] +``` + +### Choosing Relays + +**Good relays have:** +- High uptime (99%+) +- Good geographic distribution +- Support for kind 4 events (DMs) +- Read/write permissions + +**Popular Relay Lists:** +- [nostr.watch](https://nostr.watch) - Relay monitoring +- [relay.tools](https://relay.tools) - Relay explorer + +### Running Your Own Relay + +For maximum control, run your own Nostr relay: + +**Quick Setup:** +```bash +# Using nostr-rs-relay (Rust) +docker run -d -p 8080:8080 scsibug/nostr-rs-relay + +# Using strfry (C++) +# See: https://github.com/hoytech/strfry +``` + +Then add to your config: +```javascript +relays: [ + 'wss://your-relay.example.com', + // ... plus backup public relays +] +``` + +## Customization + +### Colors + +The widget uses Tailwind classes. Change colors by replacing: + +**Primary gradient** (orange): +```javascript +// Find in chat.js: +from-[#fdad01] to-[#ff8c00] + +// Replace with your brand colors: +from-[#4F46E5] to-[#7C3AED] // Purple +from-[#10B981] to-[#059669] // Green +from-[#EF4444] to-[#DC2626] // Red +``` + +**Connection indicator**: +```javascript +// Green dot when connected: +bg-green-400 + +// Change to match your theme: +bg-blue-400 +bg-purple-400 +``` + +### Position + +Change the widget position in `chat.js`: + +```javascript +// Bottom-right (default) +className="fixed bottom-4 right-4 sm:bottom-6 sm:right-6" + +// Bottom-left +className="fixed bottom-4 left-4 sm:bottom-6 sm:left-6" + +// Top-right +className="fixed top-4 right-4 sm:top-6 sm:right-6" +``` + +### Branding + +**Team Name:** +```javascript +// Find in chat.js render function: +
Loge Media Team
+ +// Replace with: +
Your Team Name
+``` + +**Header Title:** +```javascript +

Instant Chat

+ +// Change to: +

Need Help?

+``` + +## Receiving Messages + +### Setting Up Your Client + +1. **Import Your Private Key** + - Open your Nostr client + - Go to Settings → Import Key + - Paste your private key (nsec or hex) + - **NEVER share your private key** + +2. **Enable DM Notifications** + - Settings → Notifications + - Enable "Direct Messages" + - Enable push notifications (mobile) + +3. **Monitor for Messages** + - New chat sessions will appear as DMs + - Each session has a unique temporary public key + - Reply directly - users see responses instantly + +### Recommended Clients + +**Desktop/Web:** +- [Nostr.band](https://nostr.band) - Comprehensive web client +- [Nostrudel](https://nostrudel.ninja) - Feature-rich +- [Snort.social](https://snort.social) - Clean interface + +**Mobile:** +- [Damus (iOS)](https://damus.io) - Native iOS app +- [Amethyst (Android)](https://github.com/vitorpamplona/amethyst) - Powerful Android client + +**Multi-device Tips:** +- Use the same private key across devices +- Messages sync automatically via relays +- Consider a dedicated device/account for support + +## Advanced Configuration + +### Session Duration + +Change the 24-hour session expiry: + +```javascript +// In getSessionKey() function: +if (Date.now() - session.created < 24 * 60 * 60 * 1000) { + // Change to 48 hours: + if (Date.now() - session.created < 48 * 60 * 60 * 1000) { + // Change to 12 hours: + if (Date.now() - session.created < 12 * 60 * 60 * 1000) { +``` + +### Multiple Support Accounts + +Support multiple team members: + +```javascript +const CONFIG = { + relays: [...], + csPubkeys: [ + 'team-member-1-hex-pubkey', + 'team-member-2-hex-pubkey', + 'team-member-3-hex-pubkey' + ] +}; + +// Then update subscribeToReplies(): +authors: CONFIG.csPubkeys +``` + +### Custom Welcome Message + +Add an automatic welcome message: + +```javascript +// In init() function, after subscribeToReplies(): +setTimeout(() => { + addMessage('cs', 'Hi! How can we help you today?'); +}, 500); +``` + +## Troubleshooting + +### Widget Not Appearing +- Check browser console for errors +- Verify Tailwind CSS is loaded +- Ensure import map is correct +- Check `#chat-widget-root` div exists + +### Messages Not Sending +- Verify relay connections in console +- Check public key is in hex format +- Ensure relays support kind 4 (DMs) +- Try different relays + +### Not Receiving Replies +- Confirm correct private key in client +- Check relay overlap (client and widget) +- Verify DM notifications enabled +- Check client is connected to relays + +### Mobile Issues +- Clear browser cache +- Check responsive CSS is applied +- Test on actual device, not just emulator +- Verify safe-area-inset for notched devices + +## Security Best Practices + +1. **Never expose private keys** + - Only use public keys in client code + - Store private keys securely (password manager) + - Never commit keys to version control + +2. **Use HTTPS** + - Serve widget over secure connection + - Some relays require wss:// (secure websockets) + +3. **Rate limiting** + - Consider implementing rate limits + - Monitor for spam/abuse + - Block abusive session keys if needed + +4. **Session management** + - Sessions expire automatically + - No persistent user tracking + - Messages stored only in localStorage + +## Support + +Need help? Issues? Contributions? + +- **GitHub Issues**: [Report a bug](https://github.com/yourusername/nostr-chat-widget/issues) +- **Documentation**: [Full docs](https://github.com/yourusername/nostr-chat-widget) +- **Nostr**: DM us on Nostr for support \ No newline at end of file diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..b368601 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,146 @@ + + + + + + Nostr Chat Widget Demo + + + + + + +
+
+

Nostr Chat Widget

+

Privacy-first customer support powered by Nostr

+
+ 🔐 E2E Encrypted + 🌐 Decentralized + 🚫 No Backend +
+
+
+ + +
+ +
+
+

Try it now!

+

Click the orange chat button in the bottom-right corner to start a conversation

+
+ + + + Look for the chat widget → +
+
+
+ + +
+

Key Features

+
+
+
🔐
+

End-to-End Encrypted

+

All messages are encrypted using NIP-04 before transmission. Your conversations stay private.

+
+
+
🌐
+

Decentralized

+

Powered by Nostr relays. No single point of failure or control.

+
+
+
🚫
+

No Backend Required

+

Entirely client-side. Just drop in the code and you're ready to go.

+
+
+
📱
+

Mobile Responsive

+

Works seamlessly on desktop, tablet, and mobile devices.

+
+
+
+

Lightweight

+

Minimal dependencies. Fast loading and smooth performance.

+
+
+
🔑
+

Ephemeral Sessions

+

Auto-generated session keys with 24-hour expiry for enhanced privacy.

+
+
+
+ + +
+

Quick Installation

+
+
<!-- Add Tailwind CSS -->
+<script src="https://cdn.tailwindcss.com"></script>
+
+<!-- Add import map for nostr-tools -->
+<script type="importmap">
+{
+  "imports": {
+    "nostr-tools": "https://esm.sh/nostr-tools@1.17.0"
+  }
+}
+</script>
+
+<!-- Add the chat widget -->
+<script type="module" src="path/to/chat.js"></script>
+
+<!-- Add chat widget container -->
+<div id="chat-widget-root"></div>
+
+
+ + +
+

Ready to get started?

+ + + + + View on GitHub + +
+
+ + + + + + +
+ + \ No newline at end of file diff --git a/license b/license new file mode 100644 index 0000000..e9794fe --- /dev/null +++ b/license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Loge Media + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/nostr_chat_readme.md b/nostr_chat_readme.md new file mode 100644 index 0000000..9bc217d --- /dev/null +++ b/nostr_chat_readme.md @@ -0,0 +1,169 @@ +# Nostr Chat Widget + +A lightweight, privacy-focused chat widget powered by the Nostr protocol. Features end-to-end encryption, decentralized relay connections, and zero server dependencies. + +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![Nostr](https://img.shields.io/badge/protocol-Nostr-purple.svg) + +## ✨ Features + +- 🔐 **End-to-End Encrypted** - Messages encrypted using NIP-04 +- 🌐 **Decentralized** - Connects to multiple Nostr relays +- 🚫 **No Backend Required** - Entirely client-side +- 📱 **Mobile Responsive** - Works seamlessly on all devices +- ⚡ **Lightweight** - Minimal dependencies +- 🔑 **Ephemeral Keys** - Auto-generated session keys (24hr expiry) +- 💾 **Session Persistence** - Messages saved locally per session + +## 🚀 Quick Start + +### 1. Add Required Files + +Include the chat widget files in your HTML: + +```html + + + + + + + + + + +
+``` + +### 2. Configure Your Public Key + +Edit the `CONFIG` object in `chat.js`: + +```javascript +const CONFIG = { + relays: [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://nos.lol' + ], + csPubkey: 'YOUR_PUBLIC_KEY_HERE' // Your team's Nostr public key (hex format) +}; +``` + +### 3. Done! + +The chat widget will appear as a floating button in the bottom-right corner of your page. + +## 📋 Configuration Options + +| Option | Type | Description | +|--------|------|-------------| +| `relays` | Array | List of Nostr relay URLs to connect to | +| `csPubkey` | String | Your customer support team's public key (hex format) | + +### Getting Your Public Key + +1. Create a Nostr identity using any Nostr client +2. Export your public key in hex format (not npub) +3. Add it to the `csPubkey` field + +**Tip:** You can convert npub to hex at [nostr.band/tools](https://nostr.band/tools) + +## 🎨 Customization + +### Styling + +The widget uses Tailwind CSS utility classes. To customize colors, search for these classes in `chat.js`: + +- Primary color: `from-[#fdad01] to-[#ff8c00]` (orange gradient) +- Change to your brand colors + +### Mobile Behavior + +On mobile devices, the chat expands to fullscreen automatically. Customize this in the CSS: + +```css +@media (max-width: 600px) { + /* Adjust mobile styles here */ +} +``` + +## 🔒 How It Works + +1. **Session Creation**: When a user opens the chat, an ephemeral keypair is generated and stored locally +2. **Message Encryption**: All messages are encrypted using NIP-04 (end-to-end encryption) +3. **Relay Publishing**: Encrypted messages are published to multiple Nostr relays +4. **Real-time Updates**: The widget subscribes to replies from your team's public key +5. **Local Storage**: Messages persist locally for 24 hours per session + +## 📦 Dependencies + +- [nostr-tools](https://github.com/nbd-wtf/nostr-tools) - Nostr protocol implementation +- [Tailwind CSS](https://tailwindcss.com/) - Styling framework + +## 🛠️ Development + +### Local Testing + +1. Clone the repository +2. Open `demo/index.html` in a browser +3. Configure your public key in `chat.js` + +### Browser Compatibility + +- Chrome/Edge: ✅ +- Firefox: ✅ +- Safari: ✅ +- Mobile browsers: ✅ + +**Note:** Requires ES6 module support + +## 📱 Receiving Messages + +To receive and respond to chat messages, you'll need a Nostr client: + +### Recommended Clients + +- **Desktop**: [Nostr.band](https://nostr.band), [Nostrudel](https://nostrudel.ninja) +- **Mobile**: [Damus (iOS)](https://damus.io), [Amethyst (Android)](https://github.com/vitorpamplona/amethyst) +- **Web**: [Snort.social](https://snort.social), [Iris.to](https://iris.to) + +### Setup Instructions + +1. Import your private key into a Nostr client +2. Watch for DM notifications from new chat sessions +3. Reply directly from the client - messages appear instantly in the widget + +## 🔐 Security Considerations + +- **Ephemeral Keys**: Each session generates a new keypair (24hr expiry) +- **No User Data**: No tracking, cookies, or personal data collection +- **E2E Encryption**: All messages encrypted before transmission +- **Relay Privacy**: Messages distributed across multiple relays + +## 📄 License + +MIT License - See LICENSE file for details + +## 🤝 Contributing + +Contributions welcome! Please feel free to submit a Pull Request. + +## 💬 Support + +- **Issues**: [GitHub Issues](https://github.com/yourusername/nostr-chat-widget/issues) +- **Nostr**: Contact via Nostr DM + +## 🙏 Credits + +Built with [Nostr](https://nostr.com) protocol and [nostr-tools](https://github.com/nbd-wtf/nostr-tools) + +--- + +**Made with ⚡ by Loge Media** \ No newline at end of file diff --git a/package_json.json b/package_json.json new file mode 100644 index 0000000..a643422 --- /dev/null +++ b/package_json.json @@ -0,0 +1,40 @@ +{ + "name": "nostr-chat-widget", + "version": "1.0.0", + "description": "A lightweight, privacy-focused chat widget powered by the Nostr protocol with end-to-end encryption", + "main": "src/chat.js", + "type": "module", + "scripts": { + "dev": "npx live-server demo --port=8080", + "test": "echo \"No tests specified yet\" && exit 0" + }, + "keywords": [ + "nostr", + "chat", + "widget", + "e2ee", + "encrypted", + "decentralized", + "p2p", + "privacy" + ], + "author": "Loge Media", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/btcforplebs/nostr-chat-widget.git" + }, + "bugs": { + "url": "https://github.com/btcforplebs/nostr-chat-widget/issues" + }, + "homepage": "https://github.com/btcforplebs/nostr-chat-widget#readme", + "dependencies": { + "nostr-tools": "^1.17.0" + }, + "devDependencies": { + "live-server": "^1.2.2" + }, + "engines": { + "node": ">=14.0.0" + } +} \ No newline at end of file diff --git a/src/chat.js b/src/chat.js new file mode 100644 index 0000000..c06fa2d --- /dev/null +++ b/src/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();