Compare commits
10 Commits
2634007ee0
...
2c81078e77
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c81078e77 | ||
|
|
0714fcfb7a | ||
|
|
56bc8e2b92 | ||
| 640be15ac0 | |||
| 3fc9ed9c37 | |||
| 597d2f7a2c | |||
| 4aa2c76e77 | |||
| cd9dd6805f | |||
| 36f1a0d86d | |||
| 7a165d2cf4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ BTCforPlebs Website Videos/*
|
||||
node_modules/*
|
||||
package-lock.json
|
||||
public/assets/data/link-status.json
|
||||
public/local.html
|
||||
public/nostr-chat-widget-mini.js
|
||||
public/nostr-chat-widget-mini.js
|
||||
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Use an official lightweight Node image
|
||||
FROM node:20-alpine
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the project files
|
||||
COPY . .
|
||||
|
||||
# Expose the port your app uses
|
||||
EXPOSE 9000
|
||||
|
||||
# Default command to start your site
|
||||
CMD ["npm", "run", "both"]
|
||||
@@ -14,10 +14,11 @@ const links = [
|
||||
"https://nosotros.btcforplebs.com",
|
||||
"https://mint.btcforplebs.com",
|
||||
"https://cashu.btcforplebs.com",
|
||||
"https://shipyard.btcforplebs.com",
|
||||
"https://live.btcforplebs.com",
|
||||
"https://explorer.btcforplebs.com",
|
||||
"https://nsec.btcforplebs.com",
|
||||
"https://flotilla.btcforplebs.com"
|
||||
"https://flotilla.btcforplebs.com",
|
||||
"https://nutstash.btcforplebs.com",
|
||||
"https://jumble.btcforplebs.com"
|
||||
];
|
||||
|
||||
@@ -25,9 +26,9 @@ const links = [
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin || 'unknown';
|
||||
console.log(`Request: ${req.method} ${req.url} | Origin: ${origin}`);
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.sendStatus(200);
|
||||
@@ -39,8 +40,17 @@ app.get("/", (req, res) => {
|
||||
res.json({ status: "Local link status server running" });
|
||||
});
|
||||
|
||||
const cache = { data: null, timestamp: 0 };
|
||||
|
||||
app.get("/api/link-status", async (req, res) => {
|
||||
console.log("🔄 Checking link statuses...");
|
||||
const now = Date.now();
|
||||
const tenMinutes = 10 * 60 * 1000;
|
||||
if (cache.data && (now - cache.timestamp < tenMinutes)) {
|
||||
console.log("🔁 Serving from cache");
|
||||
return res.json(cache.data);
|
||||
}
|
||||
|
||||
console.log("🔄 Checking link status and refreshing cache...");
|
||||
const results = {};
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
@@ -84,6 +94,10 @@ app.get("/api/link-status", async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cache.data = results;
|
||||
cache.timestamp = now;
|
||||
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "9000:9000"
|
||||
volumes:
|
||||
- .:/app
|
||||
restart: always
|
||||
command: npm run both
|
||||
@@ -109,6 +109,13 @@
|
||||
</section>
|
||||
|
||||
<footer id="footer"></footer>
|
||||
<script src="/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
|
||||
<div id="back-to-top"><a href="#top" title="Back to Top">🔝</a></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2237
public/dashboard/dev.html
Normal file
2237
public/dashboard/dev.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/dashboard/icon.png
Normal file
BIN
public/dashboard/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
2650
public/dashboard/index.html
Normal file
2650
public/dashboard/index.html
Normal file
File diff suppressed because it is too large
Load Diff
43
public/dashboard/sw.js
Normal file
43
public/dashboard/sw.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// sw.js - Save this file in the same directory as your HTML
|
||||
const CACHE_NAME = 'nostr-dm-v1';
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/index.html'
|
||||
];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => cache.addAll(urlsToCache))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => response || fetch(event.request))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
event.notification.close();
|
||||
event.waitUntil(
|
||||
clients.openWindow('/')
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('push', event => {
|
||||
const data = event.data ? event.data.json() : {};
|
||||
const title = data.title || 'New Nostr DM';
|
||||
const options = {
|
||||
body: data.body || 'You have a new message',
|
||||
icon: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'192\' height=\'192\' viewBox=\'0 0 192 192\'%3E%3Crect width=\'192\' height=\'192\' fill=\'%23fdad01\' rx=\'24\'/%3E%3Ctext x=\'50%25\' y=\'50%25\' font-size=\'90\' fill=\'%23000\' text-anchor=\'middle\' dominant-baseline=\'middle\'%3E📬%3C/text%3E%3C/svg%3E',
|
||||
badge: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'96\' height=\'96\' viewBox=\'0 0 96 96\'%3E%3Ccircle cx=\'48\' cy=\'48\' r=\'48\' fill=\'%23fdad01\'/%3E%3Ctext x=\'50%25\' y=\'50%25\' font-size=\'50\' text-anchor=\'middle\' dominant-baseline=\'middle\'%3E📬%3C/text%3E%3C/svg%3E',
|
||||
vibrate: [200, 100, 200],
|
||||
tag: 'nostr-dm'
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
BIN
public/images/BTCforPlebsDarkMode.png
Normal file
BIN
public/images/BTCforPlebsDarkMode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -11,16 +11,17 @@
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="BTCforPlebs">
|
||||
<meta property="og:description" content="A place to help ordinary people learn about Bitcoin">
|
||||
<meta property="og:image" content="/images/thumb.jpeg">
|
||||
<meta property="og:image" content="images/thumb.jpeg">
|
||||
<meta property="og:url" content="https://btcforplebs.com">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<!-- favicon -->
|
||||
<link rel="icon" href="/images/favicon.png" type="image/png">
|
||||
<link rel="icon" href="images/favicon.png" type="image/png">
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="/assets/js/scripts.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<script src="assets/js/scripts.js"></script>
|
||||
<link rel="stylesheet" href="assets/css/main.css">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
@@ -43,19 +44,21 @@
|
||||
<div class="links" id="folder2">
|
||||
<a href="https://lightning.btcforplebs.com" target="_blank" class="prefetch">Lightning <span class="status-emoji" data-url="https://lightning.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://mempool.btcforplebs.com" target="_blank" class="prefetch">Mempool <span class="status-emoji" data-url="https://mempool.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://explorer.btcforplebs.com" target="_blank" class="prefetch">Explorer <span class="status-emoji" data-url="https://explorer.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://bitview.space/" target="_blank" class="prefetch">Bitview.space ⚪️ </a>
|
||||
<a href="https://bitfeed.live" class="prefetch">Bitfeed.Live ⚪️ </a>
|
||||
<a href="https://hodl.camp" class="prefetch">Hodl.Camp ⚪️ </a>
|
||||
<a href="https://bitview.space" class="prefetch">Bitview.Space ⚪️ </a>
|
||||
</div>
|
||||
|
||||
<!-- Nostr Links -->
|
||||
<button class="button" id="nostr-folder-btn" onclick="toggleFolder('folder1', 'nostr-folder-btn')">Use Nostr <span style="color: #F7931A;">←</span></button>
|
||||
<div class="links" id="folder1">
|
||||
<a href="https://nsec.btcforplebs.com" target="_blank" class="prefetch">Nsec Bunker <span class="status-emoji" data-url="https://nsec.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://bloom.btcforplebs.com" target="_blank" class="prefetch">Bloom <span class="status-emoji" data-url="https://bloom.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://nostrudel.btcforplebs.com" target="_blank" class="prefetch">Nostrudel <span class="status-emoji" data-url="https://nostrudel.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://nosotros.btcforplebs.com" target="_blank" class="prefetch">Nosotros <span class="status-emoji" data-url="https://nosotros.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://flotilla.btcforplebs.com" target="_blank" class="prefetch">Flotilla <span class="status-emoji" data-url="https://flotilla.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://Jumble.btcforplebs.com" target="_blank" class="prefetch">Jumble <span class="status-emoji" data-url="https://jumble.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://bloom.btcforplebs.com" target="_blank" class="prefetch">Bloom <span class="status-emoji" data-url="https://bloom.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://shipyard.btcforplebs.com" target="_blank" class="prefetch">Shipyard <span class="status-emoji" data-url="https://shipyard.btcforplebs.com">⏳</span></a>
|
||||
<a href="/relay" target="_blank" class="prefetch">Relay <span class="status-emoji" data-url="https://relay.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://nostrapps.com" target="_blank">NostrApps.com ⚪️ </a>
|
||||
</div>
|
||||
@@ -63,7 +66,8 @@
|
||||
<!-- Cashu Links -->
|
||||
<button class="button" id="cashu-folder-btn" onclick="toggleFolder('folder3', 'cashu-folder-btn')">Use Cashu <span style="color: #F7931A;">←</span></button>
|
||||
<div class="links" id="folder3">
|
||||
<a href="https://cashu.btcforplebs.com" class="prefetch">Cashu Web App <span class="status-emoji" data-url="https://cashu.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://cashu.btcforplebs.com" class="prefetch">Cashu Wallet Web App <span class="status-emoji" data-url="https://cashu.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://nutstash.btcforplebs.com" class="prefetch">Nutstash Wallet Web App <span class="status-emoji" data-url="https://nutstash.btcforplebs.com">⏳</span></a>
|
||||
<a href="/mint" class="prefetch">Cashu Mint <span class="status-emoji" data-url="https://mint.btcforplebs.com">⏳</span></a>
|
||||
<a href="https://macadamia.cash" class="prefetch">Macadamia (iOS) ⚪️ </a>
|
||||
<a href="https://minibits.cash" class="prefetch">Minibits (Android) ⚪️ </a>
|
||||
@@ -77,7 +81,11 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js" data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b" data-brand-name="Chat with BTCforPlebs" data-color="#fdad01" data-color-secondary="#8e30eb">
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -188,7 +188,12 @@
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
<script>
|
||||
// --- 2. Dropdown navigation ---------------------------------------------
|
||||
window.navigateToSection = function (select) {
|
||||
@@ -208,7 +213,8 @@
|
||||
if (sectionId) {
|
||||
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}</script>
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</html>
|
||||
@@ -62,5 +62,11 @@
|
||||
}
|
||||
}
|
||||
};</script>
|
||||
<script src="/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
<div id="back-to-top">
|
||||
<a href="#top" title="Back to Top">🔝</a>
|
||||
</div>
|
||||
<script src="/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// --- 2. Dropdown navigation ---------------------------------------------
|
||||
|
||||
@@ -105,6 +105,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer"></div>
|
||||
<script src="/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
<a href="#top" title="Back to Top">🔝</a>
|
||||
<script>function navigateToSection(select) {
|
||||
const sectionId = select.value;
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
<a href="/index.html" class="button">Home</a>
|
||||
|
||||
</div>
|
||||
<script src="/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#fdad01"
|
||||
data-color-secondary="#222222">
|
||||
</script>
|
||||
|
||||
<div id="footer"></div>
|
||||
|
||||
|
||||
@@ -8,36 +8,48 @@
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Override a bit to make it super retro */
|
||||
body {font-family: "Courier New", monospace; background:#111; color:#ecf0f1;}
|
||||
h1{color:#f39c12;}
|
||||
a {color:#8e30eb;}
|
||||
h1{color:#8e30eb;}
|
||||
.center{max-width:800px;margin:auto;padding:2rem;text-align:left;background:rgba(0,0,0,.8);border-radius:8px;}
|
||||
.btn{display:inline-block;margin:10px 0;padding:.7rem 1.5rem;background:#e67e22;color:#fff;border-radius:4px;text-decoration:none;font-weight:bold;}
|
||||
.quicklinks{margin-top:1.5rem;}
|
||||
.btn{display:inline-block;margin:10px 0;padding:.7rem 1.5rem;background:#8e30eb;color:#fff;border-radius:4px;text-decoration:none;font-weight:bold;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="center">
|
||||
<h1>BTCforPlebs Cashu Mint</h1>
|
||||
<p>Welcome to the BTCforPlebs Cashu Mint! Mint up to 100,000 sats per session – but remember, no life‑saving amounts. If you hit a snag, ping us on Nostr or email <a href="mailto:mint@btcforplebs.com">mint@btcforplebs.com</a>.</p>
|
||||
<p>Welcome to the BTCforPlebs Cashu Mint! Mint up to 100,000 sats – but remember, no life‑saving amounts. If you hit a snag, ping us on Nostr</p>
|
||||
<h2>Quick Links</h2>
|
||||
<div class="quicklinks">
|
||||
<a href="https://cashu.space" class="btn">cashu.space</a>
|
||||
<a href="https://cashu.me" class="btn">cashu.me (external host)</a>
|
||||
<a href="https://cashu.btcforplebs.com" class="btn">cashu.me (BTCforPlebs host)</a>
|
||||
<a href="https://btcforplebs.com/cashu" class="btn">btcforplebs.com/cashu</a>
|
||||
<li><a href="https://cashu.space" class="btn">cashu.space</a>
|
||||
Learn about the nuts of Cashu and how it works. <br> </br></li>
|
||||
<li><a href="https://cashu.me" class="btn">cashu.me (external host)</a>
|
||||
A Cashu PWA wallet that if feature rich! <br> </br></li>
|
||||
<li><a href="https://cashu.btcforplebs.com" class="btn">cashu.me (BTCforPlebs host)</a>
|
||||
Our own hosted version of cashu.me <br></br></li>
|
||||
<li><a href="https://btcforplebs.com/learn-cashu" class="btn">Learn Cashu</a>
|
||||
Learn what Cashu is and how to use it from BTCforPlebs<br> </br></li>
|
||||
</div>
|
||||
<h2>Minting Policy</h2>
|
||||
<br></br>
|
||||
<ul>
|
||||
<li>Maximum 100,000 sats per mint session.</li>
|
||||
<li>Do not mint your life savings.</li>
|
||||
<li>Contact us on <a href="https://nosotros.btcforplebs.com/nprofile1qydhwumn8ghj7un9d3shjtnzw33kvmmjwpkx2cnn9e3k7mgprdmhxue69uhksctkv4hzucn5vdnx7unsd3jkyuewvdhk6qgkwaehxw309aex2mrp0yh8qunfd4skctnwv46qz9rhwden5te0wfjkccte9ejxzmt4wvhxjmcqyp65vt6danj0hhj5556ulgy7krfjn0dqjz5u970ddd0e68f0kmq4kju00y6">Nostr</a></li>
|
||||
<li>Payments are processed via Cashu. See the <a href="/cashu/docs">documentation</a> for details.</li>
|
||||
</ul>
|
||||
<h2>FAQ</h2>
|
||||
<p>For frequently asked questions, visit our <a href="/cashu/faq">FAQ page</a> or join our Nostr channel.</p>
|
||||
<footer style="margin-top:2rem;color:#777;text-align:center;">
|
||||
© 2025 BTCforPlebs – Mint responsibly.
|
||||
</footer>
|
||||
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#8e30eb"
|
||||
data-color-secondary="#000000">
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1
public/nip-49decrypt/connection-test.txt
Normal file
1
public/nip-49decrypt/connection-test.txt
Normal file
@@ -0,0 +1 @@
|
||||
pong
|
||||
1737
public/nip-49decrypt/index.html
Normal file
1737
public/nip-49decrypt/index.html
Normal file
File diff suppressed because one or more lines are too long
@@ -1,15 +1,13 @@
|
||||
/**
|
||||
* Nostr Chat Widget - Embeddable Version
|
||||
* Nostr Chat Widget - Embeddable Version (Glassmorphism Design)
|
||||
*
|
||||
* HOST THIS FILE on your server (e.g., https://btcforplebs.com/chat-widget.js)
|
||||
*
|
||||
* USERS EMBED IT WITH:
|
||||
* EMBED IT WITH:
|
||||
* <script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="YOUR_PUBKEY"
|
||||
data-brand-name="My Company"
|
||||
data-color="#8e30eb">
|
||||
data-color-secondary="#ff8c00">
|
||||
</script>
|
||||
* data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
* data-brand-name="My Company"
|
||||
* data-color="#8e30eb"
|
||||
* data-color-secondary="#ff8c00">
|
||||
* </script>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
@@ -18,35 +16,118 @@
|
||||
// Get configuration from script tag
|
||||
const scriptTag = document.currentScript;
|
||||
const csPubkey = scriptTag.getAttribute('data-nostr-pubkey') || 'PUBKEY_TO_RECEICE_MESSAGES';
|
||||
const brandName = scriptTag.getAttribute('data-brand-name') || 'Support Team';
|
||||
const primaryColor = scriptTag.getAttribute('data-color-primary') || '#fdad01';
|
||||
const secondaryColor = scriptTag.getAttribute('data-color-secondary') || '#ff8c00';
|
||||
const brandName = scriptTag.getAttribute('data-brand-name') || 'Support Team Messaging';
|
||||
const primaryColor = scriptTag.getAttribute('data-color') || '#fdad01';
|
||||
const secondaryColor = scriptTag.getAttribute('data-color-secondary') || '#000000';
|
||||
|
||||
// Default relay configuration
|
||||
const DEFAULT_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.primal.net',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.btcforplebs.com',
|
||||
'wss://relay.logemedia.com'
|
||||
'wss://relay.btcforplebs.com'
|
||||
];
|
||||
|
||||
// Inject Tailwind CSS
|
||||
const tailwindLink = document.createElement('link');
|
||||
tailwindLink.href = 'https://cdn.tailwindcss.com';
|
||||
tailwindLink.rel = 'stylesheet';
|
||||
document.head.appendChild(tailwindLink);
|
||||
// Add viewport meta tag for mobile optimization
|
||||
let viewportMeta = document.querySelector('meta[name="viewport"]');
|
||||
if (!viewportMeta) {
|
||||
viewportMeta = document.createElement('meta');
|
||||
viewportMeta.name = 'viewport';
|
||||
document.head.appendChild(viewportMeta);
|
||||
}
|
||||
viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover';
|
||||
|
||||
// Inject custom styles
|
||||
// Inject custom styles with glassmorphism
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.safe-area-bottom {
|
||||
padding-bottom: max(1rem, env(safe-area-inset-bottom)) !important;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
|
||||
}
|
||||
#nostr-chat-widget-root > div {
|
||||
pointer-events: auto !important;
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.glass-morphism-light {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.glass-input:focus {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.mobile-input-container {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: transparent;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
html.chat-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
body.chat-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#nostr-chat-widget-root .chat-window-mobile {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
max-height: 100vh !important;
|
||||
max-height: 100dvh !important;
|
||||
border-radius: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
#nostr-chat-widget-root .chat-header-mobile {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
#nostr-chat-widget-root .chat-messages-mobile {
|
||||
flex: 1 1 0% !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
min-height: 0 !important;
|
||||
overscroll-behavior: contain !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
#nostr-chat-widget-root .mobile-input-container {
|
||||
position: sticky !important;
|
||||
bottom: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
z-index: 10 !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
-webkit-backdrop-filter: blur(20px) !important;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 1.5rem) !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -83,7 +164,8 @@
|
||||
relays: ${JSON.stringify(DEFAULT_RELAYS)},
|
||||
csPubkey: '${csPubkey}',
|
||||
brandName: '${brandName}',
|
||||
primaryColor: '${primaryColor}'
|
||||
primaryColor: '${primaryColor}',
|
||||
secondaryColor: '${secondaryColor}'
|
||||
};
|
||||
|
||||
let state = {
|
||||
@@ -117,14 +199,26 @@
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// Check for crypto.subtle availability (requires HTTPS)
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
state.connected = false;
|
||||
addMessage('system', '⚠️ Secure connection required. Please use HTTPS.');
|
||||
console.error('crypto.subtle not available. Page must be served over HTTPS.');
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.myPrivKey = getSessionKey();
|
||||
state.myPubKey = getPublicKey(state.myPrivKey);
|
||||
state.sessionId = state.myPubKey.substring(0, 8);
|
||||
|
||||
console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey));
|
||||
console.log('📱 User Agent:', navigator.userAgent);
|
||||
console.log('🌐 Connecting to relays...');
|
||||
|
||||
const relayPromises = CONFIG.relays.map(async (url) => {
|
||||
try {
|
||||
console.log(\`Attempting: \${url}\`);
|
||||
const relay = relayInit(url);
|
||||
|
||||
relay.on('connect', () => {
|
||||
@@ -134,6 +228,11 @@
|
||||
|
||||
relay.on('disconnect', () => {
|
||||
console.log(\`✗ Disconnected from \${url}\`);
|
||||
checkConnection();
|
||||
});
|
||||
|
||||
relay.on('error', (err) => {
|
||||
console.error(\`❌ Relay error \${url}:\`, err);
|
||||
});
|
||||
|
||||
await relay.connect();
|
||||
@@ -146,13 +245,13 @@
|
||||
|
||||
state.relays = (await Promise.all(relayPromises)).filter(r => r !== null);
|
||||
|
||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
||||
|
||||
if (state.relays.length === 0) {
|
||||
addMessage('system', '⚠️ Failed to connect to any relays');
|
||||
addMessage('system', '⚠️ Failed to connect to any relays. Check console for details.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
||||
|
||||
subscribeToReplies();
|
||||
loadPreviousMessages();
|
||||
|
||||
@@ -241,8 +340,24 @@
|
||||
|
||||
async function sendMessage() {
|
||||
if (!state.inputMessage.trim()) return;
|
||||
if (!state.connected || state.relays.length === 0) {
|
||||
addMessage('system', '⚠️ Not connected to relays. Please wait...');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageText = state.inputMessage;
|
||||
state.inputMessage = '';
|
||||
|
||||
// Show message immediately (optimistic UI)
|
||||
const tempMessage = {
|
||||
id: 'temp_' + Date.now(),
|
||||
text: messageText,
|
||||
sender: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
state.messages.push(tempMessage);
|
||||
render();
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
console.log('🔐 Encrypting and sending...');
|
||||
@@ -265,37 +380,48 @@
|
||||
event.sig = signEvent(event, state.myPrivKey);
|
||||
|
||||
let published = 0;
|
||||
for (const relay of state.relays) {
|
||||
const publishPromises = state.relays.map(async (relay) => {
|
||||
try {
|
||||
await relay.publish(event);
|
||||
published++;
|
||||
console.log(\`✓ Published to \${relay.url}\`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(publishPromises);
|
||||
|
||||
if (published === 0) {
|
||||
// Remove the temp message
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages.splice(msgIndex, 1);
|
||||
}
|
||||
addMessage('system', '⚠️ Failed to send - no relay connections');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
||||
|
||||
const message = {
|
||||
id: event.id,
|
||||
text: messageText,
|
||||
sender: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
addMessage('user', messageText, message);
|
||||
state.inputMessage = '';
|
||||
render();
|
||||
// Update temp message with real ID
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages[msgIndex].id = event.id;
|
||||
}
|
||||
saveMessages();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending:', error);
|
||||
addMessage('system', '⚠️ Failed to send message');
|
||||
// Remove the temp message on error
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages.splice(msgIndex, 1);
|
||||
}
|
||||
addMessage('system', '⚠️ Failed to send: ' + error.message);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,8 +443,13 @@
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('nostr-messages');
|
||||
if (container) {
|
||||
// Smooth scroll on desktop, instant on mobile for better keyboard handling
|
||||
if (window.innerWidth >= 640) {
|
||||
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
||||
} else {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -346,7 +477,8 @@
|
||||
container.innerHTML = \`
|
||||
<div class="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-[99999]">
|
||||
<button onclick="window.NostrChat.open()"
|
||||
class="bg-gradient-to-br from-[\${CONFIG.primaryColor}] to-[#ff8c00] hover:from-[#ff8c00] hover:to-[\${CONFIG.primaryColor}] text-white rounded-full p-4 sm:p-5 shadow-2xl transition-all transform hover:scale-110 active:scale-95"
|
||||
style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});"
|
||||
class="hover:opacity-90 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">
|
||||
@@ -359,16 +491,16 @@
|
||||
}
|
||||
|
||||
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">
|
||||
<div class="bg-gradient-to-br from-[\${CONFIG.primaryColor}] to-[#ff8c00] text-white p-4 sm:p-5">
|
||||
<div class="fixed inset-0 sm:inset-auto sm:bottom-6 sm:right-6 z-[99999]">
|
||||
<div class="glass-morphism chat-window-mobile rounded-none sm:rounded-2xl shadow-2xl w-full h-full sm:w-96 sm:h-[600px] max-w-full flex flex-col overflow-hidden">
|
||||
<div style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});" class="text-white p-3.5 sm:p-4 chat-header-mobile">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-base sm:text-lg">\${CONFIG.brandName}</h3>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<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 class="text-xs text-white/80 truncate">
|
||||
\${state.connected ? \`Encrypted • \${state.relays.length} relays\` : 'Connecting...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,9 +517,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nostr-messages" class="flex-1 overflow-y-auto p-3 sm:p-4 space-y-3 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div id="nostr-messages" class="flex-1 overflow-y-auto p-3 sm:p-3.5 space-y-3 glass-morphism-light chat-messages-mobile">
|
||||
\${state.messages.length === 0 ? \`
|
||||
<div class="text-center text-gray-400 mt-8">
|
||||
<div class="text-center text-white/60 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>
|
||||
@@ -397,7 +529,7 @@
|
||||
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">
|
||||
<div class="bg-orange-100/40 backdrop-blur-sm text-orange-800 text-xs px-3 py-2 rounded-full border border-orange-300/50">
|
||||
\${escapeHtml(msg.text)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -406,10 +538,10 @@
|
||||
return \`
|
||||
<div class="flex justify-end">
|
||||
<div class="max-w-[85%] sm:max-w-xs">
|
||||
<div class="bg-gradient-to-br from-[\${CONFIG.primaryColor}] 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">
|
||||
<div style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});" class="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 class="text-xs text-white/60 mt-1 text-right">Sent \${formatTime(msg.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
@@ -417,11 +549,10 @@
|
||||
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" style="color: \${CONFIG.primaryColor}">\${CONFIG.brandName}</div>
|
||||
<div style="background: linear-gradient(to bottom right, #9ca3af, #6b7280);" class="text-white rounded-2xl rounded-tl-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">\${formatTime(msg.timestamp)}</div>
|
||||
<div class="text-xs text-white/60 mt-1">\${formatTime(msg.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
@@ -430,21 +561,21 @@
|
||||
}).join('')}
|
||||
</div>
|
||||
|
||||
<div class="border-t bg-white p-3 sm:p-4 pb-safe">
|
||||
<div class="flex gap-2">
|
||||
<div class="p-3 sm:p-3.5 sm:mb-2 pb-6 flex-shrink-0 safe-area-bottom mobile-input-container">
|
||||
<div class="glass-input rounded-xl p-2 flex gap-2 items-center">
|
||||
<input
|
||||
id="nostr-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 text-sm sm:text-base"
|
||||
style="focus:ring-color: \${CONFIG.primaryColor}"
|
||||
class="flex-1 bg-transparent px-2 py-1.5 focus:outline-none text-sm sm:text-base text-white placeholder-white/60"
|
||||
\${!state.connected ? 'disabled' : ''}
|
||||
>
|
||||
<button
|
||||
onclick="window.NostrChat.send()"
|
||||
\${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
||||
class="bg-gradient-to-br from-[\${CONFIG.primaryColor}] to-[#ff8c00] hover:from-[#ff8c00] hover:to-[\${CONFIG.primaryColor}] 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"
|
||||
style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});"
|
||||
class="hover:opacity-90 disabled:opacity-40 text-white p-2 rounded-lg 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">
|
||||
@@ -481,14 +612,22 @@
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Only auto-focus on desktop
|
||||
if (window.innerWidth >= 640) {
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose global API
|
||||
window.NostrChat = {
|
||||
open: async () => {
|
||||
state.isOpen = true;
|
||||
// Prevent body scroll on mobile
|
||||
if (window.innerWidth < 640) {
|
||||
document.documentElement.classList.add('chat-open');
|
||||
document.body.classList.add('chat-open');
|
||||
}
|
||||
render();
|
||||
if (state.relays.length === 0) {
|
||||
await init();
|
||||
@@ -496,6 +635,9 @@
|
||||
},
|
||||
close: () => {
|
||||
state.isOpen = false;
|
||||
// Restore body scroll on mobile
|
||||
document.documentElement.classList.remove('chat-open');
|
||||
document.body.classList.remove('chat-open');
|
||||
render();
|
||||
},
|
||||
send: sendMessage
|
||||
@@ -508,7 +650,6 @@
|
||||
document.body.appendChild(widgetScript);
|
||||
})();
|
||||
|
||||
|
||||
/*!
|
||||
* fill-range <https://github.com/jonschlinkert/fill-range>
|
||||
*
|
||||
544
public/nostr-chat-widget-mini.js
Normal file
544
public/nostr-chat-widget-mini.js
Normal file
@@ -0,0 +1,544 @@
|
||||
/**
|
||||
* Nostr Chat Widget - Embeddable Version (Glassmorphism Design)
|
||||
*
|
||||
* EMBED IT WITH:
|
||||
* <script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
* data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
* data-brand-name="My Company"
|
||||
* data-color="#8e30eb"
|
||||
* data-color-secondary="#ff8c00">
|
||||
* </script>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Get configuration from script tag
|
||||
const scriptTag = document.currentScript;
|
||||
const csPubkey = scriptTag.getAttribute('data-nostr-pubkey') || 'PUBKEY_TO_RECEICE_MESSAGES';
|
||||
const brandName = scriptTag.getAttribute('data-brand-name') || 'Support Team Messaging';
|
||||
const primaryColor = scriptTag.getAttribute('data-color') || '#fdad01';
|
||||
const secondaryColor = scriptTag.getAttribute('data-color-secondary') || '#000000';
|
||||
|
||||
// Default relay configuration
|
||||
const DEFAULT_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.primal.net',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.btcforplebs.com'
|
||||
];
|
||||
|
||||
// Add viewport meta tag for mobile optimization
|
||||
let viewportMeta = document.querySelector('meta[name="viewport"]');
|
||||
if (!viewportMeta) {
|
||||
viewportMeta = document.createElement('meta');
|
||||
viewportMeta.name = 'viewport';
|
||||
viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
||||
document.head.appendChild(viewportMeta);
|
||||
}
|
||||
|
||||
|
||||
// Inject custom styles with glassmorphism
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
#nostr-chat-widget-root > div {
|
||||
pointer-events: auto !important;
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.glass-morphism-light {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.glass-input:focus {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
#nostr-chat-widget-root .chat-window-mobile {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Create widget container
|
||||
const widgetRoot = document.createElement('div');
|
||||
widgetRoot.id = 'nostr-chat-widget-root';
|
||||
document.body.appendChild(widgetRoot);
|
||||
|
||||
// Import map for nostr-tools
|
||||
const importMap = document.createElement('script');
|
||||
importMap.type = 'importmap';
|
||||
importMap.textContent = JSON.stringify({
|
||||
imports: {
|
||||
'nostr-tools': 'https://esm.sh/nostr-tools@1.17.0'
|
||||
}
|
||||
});
|
||||
document.head.appendChild(importMap);
|
||||
|
||||
// Main widget script
|
||||
const widgetScript = document.createElement('script');
|
||||
widgetScript.type = 'module';
|
||||
widgetScript.textContent = `
|
||||
import {
|
||||
relayInit,
|
||||
generatePrivateKey,
|
||||
getPublicKey,
|
||||
getEventHash,
|
||||
signEvent,
|
||||
nip19,
|
||||
nip04
|
||||
} from 'nostr-tools';
|
||||
|
||||
const CONFIG = {
|
||||
relays: ${JSON.stringify(DEFAULT_RELAYS)},
|
||||
csPubkey: '${csPubkey}',
|
||||
brandName: '${brandName}',
|
||||
primaryColor: '${primaryColor}',
|
||||
secondaryColor: '${secondaryColor}'
|
||||
};
|
||||
|
||||
let state = {
|
||||
isOpen: false,
|
||||
messages: [],
|
||||
inputMessage: '',
|
||||
myPrivKey: null,
|
||||
myPubKey: null,
|
||||
relays: [],
|
||||
connected: false,
|
||||
sessionId: null
|
||||
};
|
||||
|
||||
function getSessionKey() {
|
||||
const stored = localStorage.getItem('nostr_chat_session');
|
||||
if (stored) {
|
||||
try {
|
||||
const session = JSON.parse(stored);
|
||||
if (Date.now() - session.created < 24 * 60 * 60 * 1000) {
|
||||
return session.privKey;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const privKey = generatePrivateKey();
|
||||
localStorage.setItem('nostr_chat_session', JSON.stringify({
|
||||
privKey,
|
||||
created: Date.now()
|
||||
}));
|
||||
return privKey;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
state.myPrivKey = getSessionKey();
|
||||
state.myPubKey = getPublicKey(state.myPrivKey);
|
||||
state.sessionId = state.myPubKey.substring(0, 8);
|
||||
|
||||
console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey));
|
||||
|
||||
const relayPromises = CONFIG.relays.map(async (url) => {
|
||||
try {
|
||||
const relay = relayInit(url);
|
||||
|
||||
relay.on('connect', () => {
|
||||
console.log(\`✓ Connected to \${url}\`);
|
||||
checkConnection();
|
||||
});
|
||||
|
||||
relay.on('disconnect', () => {
|
||||
console.log(\`✗ Disconnected from \${url}\`);
|
||||
});
|
||||
|
||||
await relay.connect();
|
||||
return relay;
|
||||
} catch (error) {
|
||||
console.error(\`Failed: \${url}:\`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
state.relays = (await Promise.all(relayPromises)).filter(r => r !== null);
|
||||
|
||||
if (state.relays.length === 0) {
|
||||
addMessage('system', '⚠️ Failed to connect to any relays');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
||||
|
||||
subscribeToReplies();
|
||||
loadPreviousMessages();
|
||||
|
||||
state.connected = true;
|
||||
render();
|
||||
}
|
||||
|
||||
function checkConnection() {
|
||||
const connected = state.relays.some(r => r.status === 1);
|
||||
state.connected = connected;
|
||||
render();
|
||||
}
|
||||
|
||||
function subscribeToReplies() {
|
||||
const filters = [{
|
||||
kinds: [4],
|
||||
'#p': [state.myPubKey],
|
||||
authors: [CONFIG.csPubkey],
|
||||
since: Math.floor(Date.now() / 1000) - 86400
|
||||
}];
|
||||
|
||||
console.log('🔔 Subscribing to replies...');
|
||||
|
||||
state.relays.forEach(relay => {
|
||||
const sub = relay.sub(filters);
|
||||
|
||||
sub.on('event', (event) => {
|
||||
handleIncomingMessage(event);
|
||||
});
|
||||
|
||||
sub.on('eose', () => {
|
||||
console.log(\`✓ Subscribed: \${relay.url}\`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadPreviousMessages() {
|
||||
const stored = localStorage.getItem(\`nostr_chat_messages_\${state.sessionId}\`);
|
||||
if (stored) {
|
||||
try {
|
||||
const messages = JSON.parse(stored);
|
||||
messages.forEach(msg => state.messages.push(msg));
|
||||
render();
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function saveMessages() {
|
||||
localStorage.setItem(\`nostr_chat_messages_\${state.sessionId}\`, JSON.stringify(state.messages));
|
||||
}
|
||||
|
||||
async function handleIncomingMessage(event) {
|
||||
try {
|
||||
if (state.messages.find(m => m.id === event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📨 Received message');
|
||||
|
||||
const decryptedText = await nip04.decrypt(
|
||||
state.myPrivKey,
|
||||
event.pubkey,
|
||||
event.content
|
||||
);
|
||||
|
||||
const message = {
|
||||
id: event.id,
|
||||
text: decryptedText,
|
||||
sender: 'cs',
|
||||
timestamp: new Date(event.created_at * 1000).toISOString()
|
||||
};
|
||||
|
||||
addMessage('cs', decryptedText, message);
|
||||
|
||||
if (!document.hasFocus()) {
|
||||
const originalTitle = document.title;
|
||||
document.title = '💬 New message!';
|
||||
setTimeout(() => {
|
||||
document.title = originalTitle;
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error decrypting message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!state.inputMessage.trim()) return;
|
||||
|
||||
const messageText = state.inputMessage;
|
||||
state.inputMessage = '';
|
||||
|
||||
// Show message immediately (optimistic UI)
|
||||
const tempMessage = {
|
||||
id: 'temp_' + Date.now(),
|
||||
text: messageText,
|
||||
sender: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
state.messages.push(tempMessage);
|
||||
render();
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
console.log('🔐 Encrypting and sending...');
|
||||
|
||||
const encrypted = await nip04.encrypt(
|
||||
state.myPrivKey,
|
||||
CONFIG.csPubkey,
|
||||
messageText
|
||||
);
|
||||
|
||||
let event = {
|
||||
kind: 4,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', CONFIG.csPubkey]],
|
||||
content: encrypted,
|
||||
pubkey: state.myPubKey
|
||||
};
|
||||
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, state.myPrivKey);
|
||||
|
||||
let published = 0;
|
||||
for (const relay of state.relays) {
|
||||
try {
|
||||
await relay.publish(event);
|
||||
published++;
|
||||
console.log(\`✓ Published to \${relay.url}\`);
|
||||
} catch (err) {
|
||||
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
||||
}
|
||||
}
|
||||
|
||||
if (published === 0) {
|
||||
addMessage('system', '⚠️ Failed to send - no relay connections');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
||||
|
||||
// Update temp message with real ID
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages[msgIndex].id = event.id;
|
||||
}
|
||||
saveMessages();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending:', error);
|
||||
addMessage('system', '⚠️ Failed to send message');
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(sender, text, fullMessage = null) {
|
||||
const msg = fullMessage || {
|
||||
id: Date.now().toString(),
|
||||
text,
|
||||
sender,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
state.messages.push(msg);
|
||||
saveMessages();
|
||||
render();
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('nostr-messages');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
const container = document.getElementById('nostr-chat-widget-root');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
if (!state.isOpen) {
|
||||
container.innerHTML = \`
|
||||
<div class="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-[99999]">
|
||||
<button onclick="window.NostrChat.open()"
|
||||
style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});"
|
||||
class="hover:opacity-90 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;
|
||||
}
|
||||
|
||||
container.innerHTML = \`
|
||||
<div class="fixed inset-0 sm:inset-auto sm:bottom-6 sm:right-6 z-[99999]">
|
||||
<div class="glass-morphism chat-window-mobile rounded-none sm:rounded-2xl shadow-2xl w-full h-full sm:w-96 sm:h-[600px] max-w-full flex flex-col overflow-hidden">
|
||||
</div>
|
||||
<div style="background: linear-gradient(to bottom right, ${CONFIG.primaryColor}, ${CONFIG.secondaryColor});" class="text-white py-4 px-5 sm:px-6 rounded-t-2xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img src="/assets/logo.svg" alt="logo" class="h-5 w-5 sm:h-6 sm:w-6 rounded" />
|
||||
<h3 class="font-semibold text-sm sm:text-base">${CONFIG.brandName}</h3>
|
||||
</div>
|
||||
<button onclick="window.NostrChat.close()" class="p-2 hover:bg-white/20 rounded-md transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" 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"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div style="background: linear-gradient(to bottom right, ${CONFIG.primaryColor}, ${CONFIG.secondaryColor});" class="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-center text-white/60 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-100/40 backdrop-blur-sm text-orange-800 text-xs px-3 py-2 rounded-full border border-orange-300/50">
|
||||
\${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 style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});" class="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-white/60 mt-1 text-right">Sent \${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 style="background: linear-gradient(to bottom right, #9ca3af, #6b7280);" class="text-white rounded-2xl rounded-tl-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-white/60 mt-1">\${formatTime(msg.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
}
|
||||
return '';
|
||||
}).join('')}
|
||||
</div>
|
||||
|
||||
<div class="p-3 sm:p-3.5 mb-2 flex-shrink-0 safe-area-bottom">
|
||||
<div class="glass-input rounded-xl p-2 flex gap-2 items-center">
|
||||
<input
|
||||
id="nostr-message-input"
|
||||
type="text"
|
||||
value="\${escapeHtml(state.inputMessage)}"
|
||||
placeholder="Type your message..."
|
||||
class="flex-1 bg-transparent px-2 py-1.5 focus:outline-none text-sm sm:text-base text-white placeholder-white/60"
|
||||
\${!state.connected ? 'disabled' : ''}
|
||||
>
|
||||
<button
|
||||
onclick="window.NostrChat.send()"
|
||||
\${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
||||
style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});"
|
||||
class="hover:opacity-90 disabled:opacity-40 text-white p-2 rounded-lg 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('nostr-message-input');
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('input', (e) => {
|
||||
state.inputMessage = e.target.value;
|
||||
const sendButton = document.querySelector('button[onclick="window.NostrChat.send()"]');
|
||||
if (sendButton) {
|
||||
sendButton.disabled = !state.connected || !e.target.value.trim();
|
||||
}
|
||||
});
|
||||
messageInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
const messagesContainer = document.getElementById('nostr-messages');
|
||||
if (messagesContainer && state.messages.length > 0) {
|
||||
setTimeout(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Expose global API
|
||||
window.NostrChat = {
|
||||
open: async () => {
|
||||
state.isOpen = true;
|
||||
render();
|
||||
if (state.relays.length === 0) {
|
||||
await init();
|
||||
}
|
||||
},
|
||||
close: () => {
|
||||
state.isOpen = false;
|
||||
render();
|
||||
},
|
||||
send: sendMessage
|
||||
};
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
`;
|
||||
|
||||
document.body.appendChild(widgetScript);
|
||||
})();
|
||||
1106
public/nostr-chat-widget-nip17.js
Normal file
1106
public/nostr-chat-widget-nip17.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,52 +1,13 @@
|
||||
/**
|
||||
* Nostr Chat Widget - Embeddable Version
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
EMBED IT WITH:
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
data-brand-name="My Company"
|
||||
data-color="#8e30eb"
|
||||
data-color-secondary="#ff8c00">
|
||||
</script>
|
||||
|
||||
COLOR OPTIONS WITH:
|
||||
<!-- Purple gradient -->
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
data-brand-name="My Company"
|
||||
data-color-primary="#8e30eb"
|
||||
data-color-secondary="#5a1e9e"></script>
|
||||
|
||||
<!-- Blue gradient -->
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
data-brand-name="My Company"
|
||||
data-color-primary="#3b82f6"
|
||||
data-color-secondary="#1d4ed8"></script>
|
||||
|
||||
<!-- Green gradient -->
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
data-brand-name="My Company"
|
||||
data-color-primary="#10b981"
|
||||
data-color-secondary="#059669"></script>
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* Nostr Chat Widget - Embeddable Version (Glassmorphism Design)
|
||||
*
|
||||
* EMBED IT WITH:
|
||||
* <script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
* data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||
* data-brand-name="My Company"
|
||||
* data-color="#8e30eb"
|
||||
* data-color-secondary="#ff8c00">
|
||||
* </script>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
@@ -67,22 +28,106 @@ EMBED IT WITH:
|
||||
'wss://relay.btcforplebs.com'
|
||||
];
|
||||
|
||||
// Inject Tailwind CSS
|
||||
const tailwindLink = document.createElement('link');
|
||||
tailwindLink.href = 'https://cdn.tailwindcss.com';
|
||||
tailwindLink.rel = 'stylesheet';
|
||||
document.head.appendChild(tailwindLink);
|
||||
// Add viewport meta tag for mobile optimization
|
||||
let viewportMeta = document.querySelector('meta[name="viewport"]');
|
||||
if (!viewportMeta) {
|
||||
viewportMeta = document.createElement('meta');
|
||||
viewportMeta.name = 'viewport';
|
||||
document.head.appendChild(viewportMeta);
|
||||
}
|
||||
viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover';
|
||||
|
||||
// Inject custom styles
|
||||
// Inject custom styles with glassmorphism
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
|
||||
}
|
||||
#nostr-chat-widget-root > div {
|
||||
pointer-events: auto !important;
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
.glass-morphism {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.glass-morphism-light {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
.glass-input {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.glass-input:focus {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.glass-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.mobile-input-container {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: transparent;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
html.chat-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
body.chat-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#nostr-chat-widget-root .chat-window-mobile {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
max-height: 100vh !important;
|
||||
max-height: 100dvh !important;
|
||||
border-radius: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
#nostr-chat-widget-root .chat-header-mobile {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
#nostr-chat-widget-root .chat-messages-mobile {
|
||||
flex: 1 1 0% !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
min-height: 0 !important;
|
||||
overscroll-behavior: contain !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
#nostr-chat-widget-root .mobile-input-container {
|
||||
position: sticky !important;
|
||||
bottom: 0 !important;
|
||||
flex-shrink: 0 !important;
|
||||
z-index: 10 !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
-webkit-backdrop-filter: blur(20px) !important;
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 1.5rem) !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -154,14 +199,26 @@ EMBED IT WITH:
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// Check for crypto.subtle availability (requires HTTPS)
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
state.connected = false;
|
||||
addMessage('system', '⚠️ Secure connection required. Please use HTTPS.');
|
||||
console.error('crypto.subtle not available. Page must be served over HTTPS.');
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
state.myPrivKey = getSessionKey();
|
||||
state.myPubKey = getPublicKey(state.myPrivKey);
|
||||
state.sessionId = state.myPubKey.substring(0, 8);
|
||||
|
||||
console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey));
|
||||
console.log('📱 User Agent:', navigator.userAgent);
|
||||
console.log('🌐 Connecting to relays...');
|
||||
|
||||
const relayPromises = CONFIG.relays.map(async (url) => {
|
||||
try {
|
||||
console.log(\`Attempting: \${url}\`);
|
||||
const relay = relayInit(url);
|
||||
|
||||
relay.on('connect', () => {
|
||||
@@ -171,6 +228,11 @@ EMBED IT WITH:
|
||||
|
||||
relay.on('disconnect', () => {
|
||||
console.log(\`✗ Disconnected from \${url}\`);
|
||||
checkConnection();
|
||||
});
|
||||
|
||||
relay.on('error', (err) => {
|
||||
console.error(\`❌ Relay error \${url}:\`, err);
|
||||
});
|
||||
|
||||
await relay.connect();
|
||||
@@ -183,13 +245,13 @@ EMBED IT WITH:
|
||||
|
||||
state.relays = (await Promise.all(relayPromises)).filter(r => r !== null);
|
||||
|
||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
||||
|
||||
if (state.relays.length === 0) {
|
||||
addMessage('system', '⚠️ Failed to connect to any relays');
|
||||
addMessage('system', '⚠️ Failed to connect to any relays. Check console for details.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
||||
|
||||
subscribeToReplies();
|
||||
loadPreviousMessages();
|
||||
|
||||
@@ -278,8 +340,24 @@ EMBED IT WITH:
|
||||
|
||||
async function sendMessage() {
|
||||
if (!state.inputMessage.trim()) return;
|
||||
if (!state.connected || state.relays.length === 0) {
|
||||
addMessage('system', '⚠️ Not connected to relays. Please wait...');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageText = state.inputMessage;
|
||||
state.inputMessage = '';
|
||||
|
||||
// Show message immediately (optimistic UI)
|
||||
const tempMessage = {
|
||||
id: 'temp_' + Date.now(),
|
||||
text: messageText,
|
||||
sender: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
state.messages.push(tempMessage);
|
||||
render();
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
console.log('🔐 Encrypting and sending...');
|
||||
@@ -302,37 +380,48 @@ EMBED IT WITH:
|
||||
event.sig = signEvent(event, state.myPrivKey);
|
||||
|
||||
let published = 0;
|
||||
for (const relay of state.relays) {
|
||||
const publishPromises = state.relays.map(async (relay) => {
|
||||
try {
|
||||
await relay.publish(event);
|
||||
published++;
|
||||
console.log(\`✓ Published to \${relay.url}\`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(publishPromises);
|
||||
|
||||
if (published === 0) {
|
||||
// Remove the temp message
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages.splice(msgIndex, 1);
|
||||
}
|
||||
addMessage('system', '⚠️ Failed to send - no relay connections');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
||||
|
||||
const message = {
|
||||
id: event.id,
|
||||
text: messageText,
|
||||
sender: 'user',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
addMessage('user', messageText, message);
|
||||
state.inputMessage = '';
|
||||
render();
|
||||
// Update temp message with real ID
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages[msgIndex].id = event.id;
|
||||
}
|
||||
saveMessages();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending:', error);
|
||||
addMessage('system', '⚠️ Failed to send message');
|
||||
// Remove the temp message on error
|
||||
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||
if (msgIndex !== -1) {
|
||||
state.messages.splice(msgIndex, 1);
|
||||
}
|
||||
addMessage('system', '⚠️ Failed to send: ' + error.message);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +443,13 @@ EMBED IT WITH:
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('nostr-messages');
|
||||
if (container) {
|
||||
// Smooth scroll on desktop, instant on mobile for better keyboard handling
|
||||
if (window.innerWidth >= 640) {
|
||||
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
|
||||
} else {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -397,16 +491,16 @@ EMBED IT WITH:
|
||||
}
|
||||
|
||||
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">
|
||||
<div style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});" class="text-white p-4 sm:p-5">
|
||||
<div class="fixed inset-0 sm:inset-auto sm:bottom-6 sm:right-6 z-[99999]">
|
||||
<div class="glass-morphism chat-window-mobile rounded-none sm:rounded-2xl shadow-2xl w-full h-full sm:w-96 sm:h-[600px] max-w-full flex flex-col overflow-hidden">
|
||||
<div style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});" class="text-white p-3.5 sm:p-4 chat-header-mobile">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-bold text-base sm:text-lg">\${CONFIG.brandName}</h3>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<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 class="text-xs text-white/80 truncate">
|
||||
\${state.connected ? \`Encrypted • \${state.relays.length} relays\` : 'Connecting...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,9 +517,9 @@ EMBED IT WITH:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nostr-messages" class="flex-1 overflow-y-auto p-3 sm:p-4 space-y-3 bg-gradient-to-b from-gray-50 to-white">
|
||||
<div id="nostr-messages" class="flex-1 overflow-y-auto p-3 sm:p-3.5 space-y-3 glass-morphism-light chat-messages-mobile">
|
||||
\${state.messages.length === 0 ? \`
|
||||
<div class="text-center text-gray-400 mt-8">
|
||||
<div class="text-center text-white/60 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>
|
||||
@@ -435,7 +529,7 @@ EMBED IT WITH:
|
||||
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">
|
||||
<div class="bg-orange-100/40 backdrop-blur-sm text-orange-800 text-xs px-3 py-2 rounded-full border border-orange-300/50">
|
||||
\${escapeHtml(msg.text)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -447,7 +541,7 @@ EMBED IT WITH:
|
||||
<div style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});" class="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 class="text-xs text-white/60 mt-1 text-right">Sent \${formatTime(msg.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
@@ -455,11 +549,10 @@ EMBED IT WITH:
|
||||
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" style="color: \${CONFIG.primaryColor}">\${CONFIG.brandName}</div>
|
||||
<div style="background: linear-gradient(to bottom right, #9ca3af, #6b7280);" class="text-white rounded-2xl rounded-tl-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">\${formatTime(msg.timestamp)}</div>
|
||||
<div class="text-xs text-white/60 mt-1">\${formatTime(msg.timestamp)}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
@@ -468,24 +561,21 @@ EMBED IT WITH:
|
||||
}).join('')}
|
||||
</div>
|
||||
|
||||
<div class="border-t bg-white p-3 sm:p-4 safe-area-bottom">
|
||||
<div class="flex gap-2">
|
||||
<div class="p-3 sm:p-3.5 sm:mb-2 pb-6 flex-shrink-0 safe-area-bottom mobile-input-container">
|
||||
<div class="glass-input rounded-xl p-2 flex gap-2 items-center">
|
||||
<input
|
||||
id="nostr-message-input"
|
||||
type="text"
|
||||
value="\${escapeHtml(state.inputMessage)}"
|
||||
placeholder="Type your message..."
|
||||
style="border-color: #d1d5db;"
|
||||
class="flex-1 px-3 py-2 sm:px-4 sm:py-3 border rounded-xl focus:outline-none focus:ring-2 text-sm sm:text-base text-black"
|
||||
onfocus="this.style.borderColor='\${CONFIG.primaryColor}'; this.style.boxShadow='0 0 0 2px \${CONFIG.primaryColor}33';"
|
||||
onblur="this.style.borderColor='#d1d5db'; this.style.boxShadow='none';"
|
||||
class="flex-1 bg-transparent px-2 py-1.5 focus:outline-none text-sm sm:text-base text-white placeholder-white/60"
|
||||
\${!state.connected ? 'disabled' : ''}
|
||||
>
|
||||
<button
|
||||
onclick="window.NostrChat.send()"
|
||||
\${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
||||
style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});"
|
||||
class="hover:opacity-90 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 disabled:opacity-50"
|
||||
class="hover:opacity-90 disabled:opacity-40 text-white p-2 rounded-lg 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">
|
||||
@@ -522,14 +612,22 @@ EMBED IT WITH:
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Only auto-focus on desktop
|
||||
if (window.innerWidth >= 640) {
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose global API
|
||||
window.NostrChat = {
|
||||
open: async () => {
|
||||
state.isOpen = true;
|
||||
// Prevent body scroll on mobile
|
||||
if (window.innerWidth < 640) {
|
||||
document.documentElement.classList.add('chat-open');
|
||||
document.body.classList.add('chat-open');
|
||||
}
|
||||
render();
|
||||
if (state.relays.length === 0) {
|
||||
await init();
|
||||
@@ -537,6 +635,9 @@ EMBED IT WITH:
|
||||
},
|
||||
close: () => {
|
||||
state.isOpen = false;
|
||||
// Restore body scroll on mobile
|
||||
document.documentElement.classList.remove('chat-open');
|
||||
document.body.classList.remove('chat-open');
|
||||
render();
|
||||
},
|
||||
send: sendMessage
|
||||
@@ -549,7 +650,6 @@ EMBED IT WITH:
|
||||
document.body.appendChild(widgetScript);
|
||||
})();
|
||||
|
||||
|
||||
/*!
|
||||
* fill-range <https://github.com/jonschlinkert/fill-range>
|
||||
*
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<p>Follow BTCforPlebs on <a href="https://nostrudel.btcforplebs.com/u/npub1w4rz7n0vunaau499xh86p84s6v5mmgys48p0nmttt7w36takc9dsf4382j">Nostr</a> or <a href="https://youtube.com/@btcforplebs">YouTube</a> for the latest</p>
|
||||
</div>
|
||||
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||
<script src="https://btcforplebs.com/nostr-chat-widget-WIP.js"
|
||||
data-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||
data-brand-name="Chat with BTCforPlebs"
|
||||
data-color="#8e30eb"
|
||||
|
||||
Reference in New Issue
Block a user