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,33 +14,43 @@ 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"
|
||||
];
|
||||
|
||||
// Optional: Only allow internal requests if you're not exposing this publicly
|
||||
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-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);
|
||||
}
|
||||
next();
|
||||
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");
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
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
|
||||
@@ -108,7 +108,14 @@
|
||||
<p>Week 6 wraps up the series with a practical, faith‑first guide to the next steps in a Bitcoin journey. Logan reviews the final podcast episodes, outlines how to move from “just learning” to owning, securing, and sharing Bitcoin, and reminds listeners that the true goal is to place Christ first, practice generosity, and build a financial legacy that glorifies God. The session ends with a heartfelt prayer and an invitation for anyone to step into the gospel—one small step at a time.</p>
|
||||
</section>
|
||||
|
||||
<footer id="footer"></footer>
|
||||
<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,8 +81,12 @@
|
||||
</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>
|
||||
<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>
|
||||
// emoji status fetcher
|
||||
|
||||
@@ -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:
|
||||
* <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>
|
||||
* 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() {
|
||||
@@ -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,7 +443,12 @@
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('nostr-messages');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
// 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,7 +612,10 @@
|
||||
}, 100);
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
// Only auto-focus on desktop
|
||||
if (window.innerWidth >= 640) {
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,6 +623,11 @@
|
||||
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>
|
||||
*
|
||||
@@ -594,4 +735,4 @@ Check your Browserslist config to be sure that your targets are set up correctly
|
||||
https://github.com/postcss/autoprefixer#readme
|
||||
https://github.com/browserslist/browserslist#readme
|
||||
|
||||
`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})();
|
||||
`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})();
|
||||
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,7 +443,12 @@ EMBED IT WITH:
|
||||
setTimeout(() => {
|
||||
const container = document.getElementById('nostr-messages');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
// 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,7 +612,10 @@ EMBED IT WITH:
|
||||
}, 100);
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
// Only auto-focus on desktop
|
||||
if (window.innerWidth >= 640) {
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +623,11 @@ EMBED IT WITH:
|
||||
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>
|
||||
*
|
||||
@@ -635,4 +735,4 @@ Check your Browserslist config to be sure that your targets are set up correctly
|
||||
https://github.com/postcss/autoprefixer#readme
|
||||
https://github.com/browserslist/browserslist#readme
|
||||
|
||||
`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})();
|
||||
`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})();
|
||||
@@ -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