Initial commit: Nostr chat widget
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -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/
|
||||||
226
chat.css
Normal file
226
chat.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
343
config_docs.md
Normal file
343
config_docs.md
Normal file
@@ -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 `<head>`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Tailwind CSS for styling -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- Import map for nostr-tools -->
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"nostr-tools": "https://esm.sh/nostr-tools@1.17.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Add the Widget
|
||||||
|
|
||||||
|
Before your closing `</body>` tag:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Chat widget script -->
|
||||||
|
<script type="module" src="path/to/chat.js"></script>
|
||||||
|
|
||||||
|
<!-- Chat widget container -->
|
||||||
|
<div id="chat-widget-root"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
<div class="text-xs font-semibold text-[#fdad01] mb-1">Loge Media Team</div>
|
||||||
|
|
||||||
|
// Replace with:
|
||||||
|
<div class="text-xs font-semibold text-[#fdad01] mb-1">Your Team Name</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Header Title:**
|
||||||
|
```javascript
|
||||||
|
<h3 class="font-bold text-base sm:text-lg">Instant Chat</h3>
|
||||||
|
|
||||||
|
// Change to:
|
||||||
|
<h3 class="font-bold text-base sm:text-lg">Need Help?</h3>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
146
demo/index.html
Normal file
146
demo/index.html
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nostr Chat Widget Demo</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"nostr-tools": "https://esm.sh/nostr-tools@1.17.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.feature-card {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gradient-bg text-white py-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<h1 class="text-5xl font-bold mb-4">Nostr Chat Widget</h1>
|
||||||
|
<p class="text-xl opacity-90">Privacy-first customer support powered by Nostr</p>
|
||||||
|
<div class="mt-8 flex gap-4 justify-center">
|
||||||
|
<span class="bg-white/20 px-4 py-2 rounded-full text-sm">🔐 E2E Encrypted</span>
|
||||||
|
<span class="bg-white/20 px-4 py-2 rounded-full text-sm">🌐 Decentralized</span>
|
||||||
|
<span class="bg-white/20 px-4 py-2 rounded-full text-sm">🚫 No Backend</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="container mx-auto px-4 py-16">
|
||||||
|
<!-- Demo Section -->
|
||||||
|
<section class="max-w-4xl mx-auto mb-20">
|
||||||
|
<div class="bg-gradient-to-r from-purple-500 to-pink-500 text-white p-8 rounded-2xl shadow-xl text-center">
|
||||||
|
<h2 class="text-3xl font-bold mb-4">Try it now!</h2>
|
||||||
|
<p class="text-lg mb-6">Click the orange chat button in the bottom-right corner to start a conversation</p>
|
||||||
|
<div class="inline-flex items-center gap-2 bg-white/20 px-6 py-3 rounded-full">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Look for the chat widget →</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features Grid -->
|
||||||
|
<section class="max-w-6xl mx-auto mb-20">
|
||||||
|
<h2 class="text-3xl font-bold text-center mb-12 text-gray-800">Key Features</h2>
|
||||||
|
<div class="grid md:grid-cols-3 gap-8">
|
||||||
|
<div class="feature-card bg-white p-6 rounded-xl shadow-lg">
|
||||||
|
<div class="text-4xl mb-4">🔐</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">End-to-End Encrypted</h3>
|
||||||
|
<p class="text-gray-600">All messages are encrypted using NIP-04 before transmission. Your conversations stay private.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card bg-white p-6 rounded-xl shadow-lg">
|
||||||
|
<div class="text-4xl mb-4">🌐</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">Decentralized</h3>
|
||||||
|
<p class="text-gray-600">Powered by Nostr relays. No single point of failure or control.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card bg-white p-6 rounded-xl shadow-lg">
|
||||||
|
<div class="text-4xl mb-4">🚫</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">No Backend Required</h3>
|
||||||
|
<p class="text-gray-600">Entirely client-side. Just drop in the code and you're ready to go.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card bg-white p-6 rounded-xl shadow-lg">
|
||||||
|
<div class="text-4xl mb-4">📱</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">Mobile Responsive</h3>
|
||||||
|
<p class="text-gray-600">Works seamlessly on desktop, tablet, and mobile devices.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card bg-white p-6 rounded-xl shadow-lg">
|
||||||
|
<div class="text-4xl mb-4">⚡</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">Lightweight</h3>
|
||||||
|
<p class="text-gray-600">Minimal dependencies. Fast loading and smooth performance.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card bg-white p-6 rounded-xl shadow-lg">
|
||||||
|
<div class="text-4xl mb-4">🔑</div>
|
||||||
|
<h3 class="text-xl font-bold mb-2">Ephemeral Sessions</h3>
|
||||||
|
<p class="text-gray-600">Auto-generated session keys with 24-hour expiry for enhanced privacy.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Installation -->
|
||||||
|
<section class="max-w-4xl mx-auto mb-20">
|
||||||
|
<h2 class="text-3xl font-bold text-center mb-8 text-gray-800">Quick Installation</h2>
|
||||||
|
<div class="bg-gray-900 text-gray-100 p-6 rounded-xl overflow-x-auto">
|
||||||
|
<pre class="text-sm"><code><!-- 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></code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- GitHub Link -->
|
||||||
|
<section class="text-center">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-gray-800">Ready to get started?</h2>
|
||||||
|
<a href="https://github.com/yourusername/nostr-chat-widget"
|
||||||
|
class="inline-flex items-center gap-2 bg-gray-900 text-white px-8 py-4 rounded-xl font-semibold hover:bg-gray-800 transition">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||||
|
</svg>
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="bg-gray-900 text-white py-8 mt-20">
|
||||||
|
<div class="container mx-auto px-4 text-center">
|
||||||
|
<p class="text-gray-400">Built with ⚡ by Loge Media</p>
|
||||||
|
<p class="text-sm text-gray-500 mt-2">Powered by the Nostr protocol</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Chat Widget -->
|
||||||
|
<script type="module" src="../src/chat.js"></script>
|
||||||
|
<div id="chat-widget-root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
license
Normal file
21
license
Normal file
@@ -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.
|
||||||
169
nostr_chat_readme.md
Normal file
169
nostr_chat_readme.md
Normal file
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ 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
|
||||||
|
<!-- Add Tailwind CSS for styling -->
|
||||||
|
<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>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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**
|
||||||
40
package_json.json
Normal file
40
package_json.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
466
src/chat.js
Normal file
466
src/chat.js
Normal file
@@ -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 = `
|
||||||
|
<div class="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-[99999]">
|
||||||
|
<button onclick="window.openChat()"
|
||||||
|
class="bg-gradient-to-br from-[#fdad01] to-[#ff8c00] hover:from-[#ff8c00] hover:to-[#fdad01] text-white rounded-full p-4 sm:p-5 shadow-2xl transition-all transform hover:scale-110 active:scale-95"
|
||||||
|
aria-label="Open chat"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="sm:w-7 sm:h-7">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full chat box rendering with mobile responsiveness
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="fixed inset-4 sm:inset-auto sm:bottom-6 sm:right-6 z-[99999]">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl w-full h-full sm:w-96 sm:h-[600px] max-w-full flex flex-col overflow-hidden">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-gradient-to-br from-[#fdad01] to-[#ff8c00] text-white p-4 sm:p-5">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-bold text-base sm:text-lg">Instant Chat</h3>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<div class="w-2 h-2 rounded-full flex-shrink-0 ${state.connected ? 'bg-green-400 animate-pulse' : 'bg-red-400'}"></div>
|
||||||
|
<span class="text-xs text-orange-100 truncate">
|
||||||
|
${state.connected ? `P2P E2EE • ${state.relays.length} relays` : 'Connecting...'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick="window.closeChat()"
|
||||||
|
class="hover:bg-white/20 p-2 rounded-lg transition-colors ml-2 flex-shrink-0"
|
||||||
|
aria-label="Close chat"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<div id="messages" class="flex-1 overflow-y-auto p-3 sm:p-4 space-y-3 bg-gradient-to-b from-gray-50 to-white">
|
||||||
|
${state.messages.length === 0 ? `
|
||||||
|
<div class="text-center text-gray-400 mt-8">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" class="mx-auto mb-3 opacity-50">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm">Start a conversation</p>
|
||||||
|
</div>
|
||||||
|
` : state.messages.map(msg => {
|
||||||
|
if (msg.sender === 'system') {
|
||||||
|
return `
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="bg-orange-50 text-orange-700 text-xs px-3 py-2 rounded-full border border-orange-200">
|
||||||
|
${escapeHtml(msg.text)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (msg.sender === 'user') {
|
||||||
|
return `
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="max-w-[85%] sm:max-w-xs">
|
||||||
|
<div class="bg-gradient-to-br from-[#fdad01] to-[#ff8c00] text-white rounded-2xl rounded-tr-sm px-3 py-2 sm:px-4 sm:py-3 shadow-md text-sm sm:text-base">
|
||||||
|
${escapeHtml(msg.text)}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1 text-right">${formatTime(msg.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (msg.sender === 'cs') {
|
||||||
|
return `
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<div class="max-w-[85%] sm:max-w-xs">
|
||||||
|
<div class="bg-white border border-gray-200 text-gray-800 rounded-2xl rounded-tl-sm px-3 py-2 sm:px-4 sm:py-3 shadow-md text-sm sm:text-base">
|
||||||
|
<div class="text-xs font-semibold text-[#fdad01] mb-1">Loge Media Team</div>
|
||||||
|
${escapeHtml(msg.text)}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">${formatTime(msg.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input -->
|
||||||
|
<div class="border-t bg-white p-3 sm:p-4 safe-area-bottom">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
id="message-input"
|
||||||
|
type="text"
|
||||||
|
value="${escapeHtml(state.inputMessage)}"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
class="flex-1 px-3 py-2 sm:px-4 sm:py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#fdad01] text-sm sm:text-base"
|
||||||
|
${!state.connected ? 'disabled' : ''}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick="window.sendMessage()"
|
||||||
|
${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
||||||
|
class="bg-gradient-to-br from-[#fdad01] to-[#ff8c00] hover:from-[#ff8c00] hover:to-[#fdad01] disabled:from-gray-400 disabled:to-gray-400 text-white px-4 py-2 sm:px-5 sm:py-3 rounded-xl transition-all disabled:cursor-not-allowed active:scale-95 flex-shrink-0"
|
||||||
|
aria-label="Send message"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||||
|
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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();
|
||||||
Reference in New Issue
Block a user