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/*
|
node_modules/*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
public/assets/data/link-status.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://nosotros.btcforplebs.com",
|
||||||
"https://mint.btcforplebs.com",
|
"https://mint.btcforplebs.com",
|
||||||
"https://cashu.btcforplebs.com",
|
"https://cashu.btcforplebs.com",
|
||||||
|
"https://shipyard.btcforplebs.com",
|
||||||
"https://live.btcforplebs.com",
|
"https://live.btcforplebs.com",
|
||||||
"https://explorer.btcforplebs.com",
|
|
||||||
"https://nsec.btcforplebs.com",
|
"https://nsec.btcforplebs.com",
|
||||||
"https://flotilla.btcforplebs.com"
|
"https://flotilla.btcforplebs.com",
|
||||||
|
"https://nutstash.btcforplebs.com",
|
||||||
"https://jumble.btcforplebs.com"
|
"https://jumble.btcforplebs.com"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -25,9 +26,9 @@ const links = [
|
|||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const origin = req.headers.origin || 'unknown';
|
const origin = req.headers.origin || 'unknown';
|
||||||
console.log(`Request: ${req.method} ${req.url} | Origin: ${origin}`);
|
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-Origin", "*");
|
||||||
res.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
res.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||||
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
@@ -39,8 +40,17 @@ app.get("/", (req, res) => {
|
|||||||
res.json({ status: "Local link status server running" });
|
res.json({ status: "Local link status server running" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cache = { data: null, timestamp: 0 };
|
||||||
|
|
||||||
app.get("/api/link-status", async (req, res) => {
|
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 results = {};
|
||||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
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);
|
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>
|
</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>
|
<div id="back-to-top"><a href="#top" title="Back to Top">🔝</a></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="BTCforPlebs">
|
<meta property="og:title" content="BTCforPlebs">
|
||||||
<meta property="og:description" content="A place to help ordinary people learn about Bitcoin">
|
<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 property="og:url" content="https://btcforplebs.com">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
|
||||||
<!-- favicon -->
|
<!-- favicon -->
|
||||||
<link rel="icon" href="/images/favicon.png" type="image/png">
|
<link rel="icon" href="images/favicon.png" type="image/png">
|
||||||
|
|
||||||
<!-- scripts -->
|
<!-- scripts -->
|
||||||
<script src="/assets/js/scripts.js"></script>
|
<script src="assets/js/scripts.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/css/main.css">
|
<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">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -43,19 +44,21 @@
|
|||||||
<div class="links" id="folder2">
|
<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://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://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://bitfeed.live" class="prefetch">Bitfeed.Live ⚪️ </a>
|
||||||
<a href="https://bitview.space/" target="_blank" class="prefetch">Bitview.space ⚪️ </a>
|
<a href="https://hodl.camp" class="prefetch">Hodl.Camp ⚪️ </a>
|
||||||
|
<a href="https://bitview.space" class="prefetch">Bitview.Space ⚪️ </a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nostr Links -->
|
<!-- Nostr Links -->
|
||||||
<button class="button" id="nostr-folder-btn" onclick="toggleFolder('folder1', 'nostr-folder-btn')">Use Nostr <span style="color: #F7931A;">←</span></button>
|
<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">
|
<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://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://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://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://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://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="/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>
|
<a href="https://nostrapps.com" target="_blank">NostrApps.com ⚪️ </a>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +66,8 @@
|
|||||||
<!-- Cashu Links -->
|
<!-- Cashu Links -->
|
||||||
<button class="button" id="cashu-folder-btn" onclick="toggleFolder('folder3', 'cashu-folder-btn')">Use Cashu <span style="color: #F7931A;">←</span></button>
|
<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">
|
<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="/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://macadamia.cash" class="prefetch">Macadamia (iOS) ⚪️ </a>
|
||||||
<a href="https://minibits.cash" class="prefetch">Minibits (Android) ⚪️ </a>
|
<a href="https://minibits.cash" class="prefetch">Minibits (Android) ⚪️ </a>
|
||||||
@@ -77,7 +81,11 @@
|
|||||||
</div>
|
</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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -188,7 +188,12 @@
|
|||||||
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
// --- 2. Dropdown navigation ---------------------------------------------
|
// --- 2. Dropdown navigation ---------------------------------------------
|
||||||
window.navigateToSection = function (select) {
|
window.navigateToSection = function (select) {
|
||||||
@@ -208,7 +213,8 @@
|
|||||||
if (sectionId) {
|
if (sectionId) {
|
||||||
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' });
|
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}</script>
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -62,5 +62,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};</script>
|
};</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
<div id="back-to-top">
|
<div id="back-to-top">
|
||||||
<a href="#top" title="Back to Top">🔝</a>
|
<a href="#top" title="Back to Top">🔝</a>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
// --- 2. Dropdown navigation ---------------------------------------------
|
// --- 2. Dropdown navigation ---------------------------------------------
|
||||||
|
|||||||
@@ -105,6 +105,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="footer"></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>
|
<a href="#top" title="Back to Top">🔝</a>
|
||||||
<script>function navigateToSection(select) {
|
<script>function navigateToSection(select) {
|
||||||
const sectionId = select.value;
|
const sectionId = select.value;
|
||||||
|
|||||||
@@ -44,6 +44,12 @@
|
|||||||
<a href="/index.html" class="button">Home</a>
|
<a href="/index.html" class="button">Home</a>
|
||||||
|
|
||||||
</div>
|
</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>
|
<div id="footer"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,36 +8,48 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/main.css">
|
<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">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
/* Override a bit to make it super retro */
|
||||||
body {font-family: "Courier New", monospace; background:#111; color:#ecf0f1;}
|
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;}
|
.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;}
|
.btn{display:inline-block;margin:10px 0;padding:.7rem 1.5rem;background:#8e30eb;color:#fff;border-radius:4px;text-decoration:none;font-weight:bold;}
|
||||||
.quicklinks{margin-top:1.5rem;}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<h1>BTCforPlebs Cashu Mint</h1>
|
<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>
|
<h2>Quick Links</h2>
|
||||||
<div class="quicklinks">
|
<div class="quicklinks">
|
||||||
<a href="https://cashu.space" class="btn">cashu.space</a>
|
<li><a href="https://cashu.space" class="btn">cashu.space</a>
|
||||||
<a href="https://cashu.me" class="btn">cashu.me (external host)</a>
|
Learn about the nuts of Cashu and how it works. <br> </br></li>
|
||||||
<a href="https://cashu.btcforplebs.com" class="btn">cashu.me (BTCforPlebs host)</a>
|
<li><a href="https://cashu.me" class="btn">cashu.me (external host)</a>
|
||||||
<a href="https://btcforplebs.com/cashu" class="btn">btcforplebs.com/cashu</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>
|
</div>
|
||||||
<h2>Minting Policy</h2>
|
<h2>Minting Policy</h2>
|
||||||
|
<br></br>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Maximum 100,000 sats per mint session.</li>
|
<li>Maximum 100,000 sats per mint session.</li>
|
||||||
<li>Do not mint your life savings.</li>
|
<li>Do not mint your life savings.</li>
|
||||||
<li>Contact us on <a href="https://nosotros.btcforplebs.com/nprofile1qydhwumn8ghj7un9d3shjtnzw33kvmmjwpkx2cnn9e3k7mgprdmhxue69uhksctkv4hzucn5vdnx7unsd3jkyuewvdhk6qgkwaehxw309aex2mrp0yh8qunfd4skctnwv46qz9rhwden5te0wfjkccte9ejxzmt4wvhxjmcqyp65vt6danj0hhj5556ulgy7krfjn0dqjz5u970ddd0e68f0kmq4kju00y6">Nostr</a></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>
|
<li>Payments are processed via Cashu. See the <a href="/cashu/docs">documentation</a> for details.</li>
|
||||||
</ul>
|
</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;">
|
<footer style="margin-top:2rem;color:#777;text-align:center;">
|
||||||
© 2025 BTCforPlebs – Mint responsibly.
|
© 2025 BTCforPlebs – Mint responsibly.
|
||||||
</footer>
|
</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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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)
|
* EMBED IT WITH:
|
||||||
*
|
|
||||||
* USERS EMBED IT WITH:
|
|
||||||
* <script src="https://btcforplebs.com/nostr-chat-widget.js"
|
* <script src="https://btcforplebs.com/nostr-chat-widget.js"
|
||||||
data-nostr-pubkey="YOUR_PUBKEY"
|
* data-nostr-pubkey="YOUR_PUBKEY_HEX_FORMAT"
|
||||||
data-brand-name="My Company"
|
* data-brand-name="My Company"
|
||||||
data-color="#8e30eb">
|
* data-color="#8e30eb"
|
||||||
data-color-secondary="#ff8c00">
|
* data-color-secondary="#ff8c00">
|
||||||
</script>
|
* </script>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
@@ -18,35 +16,118 @@
|
|||||||
// Get configuration from script tag
|
// Get configuration from script tag
|
||||||
const scriptTag = document.currentScript;
|
const scriptTag = document.currentScript;
|
||||||
const csPubkey = scriptTag.getAttribute('data-nostr-pubkey') || 'PUBKEY_TO_RECEICE_MESSAGES';
|
const csPubkey = scriptTag.getAttribute('data-nostr-pubkey') || 'PUBKEY_TO_RECEICE_MESSAGES';
|
||||||
const brandName = scriptTag.getAttribute('data-brand-name') || 'Support Team';
|
const brandName = scriptTag.getAttribute('data-brand-name') || 'Support Team Messaging';
|
||||||
const primaryColor = scriptTag.getAttribute('data-color-primary') || '#fdad01';
|
const primaryColor = scriptTag.getAttribute('data-color') || '#fdad01';
|
||||||
const secondaryColor = scriptTag.getAttribute('data-color-secondary') || '#ff8c00';
|
const secondaryColor = scriptTag.getAttribute('data-color-secondary') || '#000000';
|
||||||
|
|
||||||
// Default relay configuration
|
// Default relay configuration
|
||||||
const DEFAULT_RELAYS = [
|
const DEFAULT_RELAYS = [
|
||||||
'wss://relay.damus.io',
|
'wss://relay.damus.io',
|
||||||
'wss://relay.primal.net',
|
'wss://relay.primal.net',
|
||||||
'wss://nos.lol',
|
'wss://nos.lol',
|
||||||
'wss://relay.btcforplebs.com',
|
'wss://relay.btcforplebs.com'
|
||||||
'wss://relay.logemedia.com'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Inject Tailwind CSS
|
// Add viewport meta tag for mobile optimization
|
||||||
const tailwindLink = document.createElement('link');
|
let viewportMeta = document.querySelector('meta[name="viewport"]');
|
||||||
tailwindLink.href = 'https://cdn.tailwindcss.com';
|
if (!viewportMeta) {
|
||||||
tailwindLink.rel = 'stylesheet';
|
viewportMeta = document.createElement('meta');
|
||||||
document.head.appendChild(tailwindLink);
|
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');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.safe-area-bottom {
|
.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 {
|
#nostr-chat-widget-root > div {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
z-index: 99999 !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);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
@@ -83,7 +164,8 @@
|
|||||||
relays: ${JSON.stringify(DEFAULT_RELAYS)},
|
relays: ${JSON.stringify(DEFAULT_RELAYS)},
|
||||||
csPubkey: '${csPubkey}',
|
csPubkey: '${csPubkey}',
|
||||||
brandName: '${brandName}',
|
brandName: '${brandName}',
|
||||||
primaryColor: '${primaryColor}'
|
primaryColor: '${primaryColor}',
|
||||||
|
secondaryColor: '${secondaryColor}'
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
@@ -117,14 +199,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
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.myPrivKey = getSessionKey();
|
||||||
state.myPubKey = getPublicKey(state.myPrivKey);
|
state.myPubKey = getPublicKey(state.myPrivKey);
|
||||||
state.sessionId = state.myPubKey.substring(0, 8);
|
state.sessionId = state.myPubKey.substring(0, 8);
|
||||||
|
|
||||||
console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey));
|
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) => {
|
const relayPromises = CONFIG.relays.map(async (url) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(\`Attempting: \${url}\`);
|
||||||
const relay = relayInit(url);
|
const relay = relayInit(url);
|
||||||
|
|
||||||
relay.on('connect', () => {
|
relay.on('connect', () => {
|
||||||
@@ -134,6 +228,11 @@
|
|||||||
|
|
||||||
relay.on('disconnect', () => {
|
relay.on('disconnect', () => {
|
||||||
console.log(\`✗ Disconnected from \${url}\`);
|
console.log(\`✗ Disconnected from \${url}\`);
|
||||||
|
checkConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
relay.on('error', (err) => {
|
||||||
|
console.error(\`❌ Relay error \${url}:\`, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
await relay.connect();
|
await relay.connect();
|
||||||
@@ -146,13 +245,13 @@
|
|||||||
|
|
||||||
state.relays = (await Promise.all(relayPromises)).filter(r => r !== null);
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
|
||||||
|
|
||||||
subscribeToReplies();
|
subscribeToReplies();
|
||||||
loadPreviousMessages();
|
loadPreviousMessages();
|
||||||
|
|
||||||
@@ -241,8 +340,24 @@
|
|||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
if (!state.inputMessage.trim()) return;
|
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;
|
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 {
|
try {
|
||||||
console.log('🔐 Encrypting and sending...');
|
console.log('🔐 Encrypting and sending...');
|
||||||
@@ -265,37 +380,48 @@
|
|||||||
event.sig = signEvent(event, state.myPrivKey);
|
event.sig = signEvent(event, state.myPrivKey);
|
||||||
|
|
||||||
let published = 0;
|
let published = 0;
|
||||||
for (const relay of state.relays) {
|
const publishPromises = state.relays.map(async (relay) => {
|
||||||
try {
|
try {
|
||||||
await relay.publish(event);
|
await relay.publish(event);
|
||||||
published++;
|
published++;
|
||||||
console.log(\`✓ Published to \${relay.url}\`);
|
console.log(\`✓ Published to \${relay.url}\`);
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
await Promise.all(publishPromises);
|
||||||
|
|
||||||
if (published === 0) {
|
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');
|
addMessage('system', '⚠️ Failed to send - no relay connections');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
||||||
|
|
||||||
const message = {
|
// Update temp message with real ID
|
||||||
id: event.id,
|
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||||
text: messageText,
|
if (msgIndex !== -1) {
|
||||||
sender: 'user',
|
state.messages[msgIndex].id = event.id;
|
||||||
timestamp: new Date().toISOString()
|
}
|
||||||
};
|
saveMessages();
|
||||||
|
|
||||||
addMessage('user', messageText, message);
|
|
||||||
state.inputMessage = '';
|
|
||||||
render();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending:', 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(() => {
|
setTimeout(() => {
|
||||||
const container = document.getElementById('nostr-messages');
|
const container = document.getElementById('nostr-messages');
|
||||||
if (container) {
|
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;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +477,8 @@
|
|||||||
container.innerHTML = \`
|
container.innerHTML = \`
|
||||||
<div class="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-[99999]">
|
<div class="fixed bottom-4 right-4 sm:bottom-6 sm:right-6 z-[99999]">
|
||||||
<button onclick="window.NostrChat.open()"
|
<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"
|
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">
|
<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 = \`
|
container.innerHTML = \`
|
||||||
<div class="fixed inset-4 sm:inset-auto sm:bottom-6 sm:right-6 z-[99999]">
|
<div class="fixed inset-0 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="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 class="bg-gradient-to-br from-[\${CONFIG.primaryColor}] to-[#ff8c00] text-white p-4 sm:p-5">
|
<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 justify-between items-center">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="font-bold text-base sm:text-lg">\${CONFIG.brandName}</h3>
|
<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>
|
<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">
|
<span class="text-xs text-white/80 truncate">
|
||||||
\${state.connected ? \`P2P E2EE • \${state.relays.length} relays\` : 'Connecting...'}
|
\${state.connected ? \`Encrypted • \${state.relays.length} relays\` : 'Connecting...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,9 +517,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 ? \`
|
\${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">
|
<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>
|
<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>
|
</svg>
|
||||||
@@ -397,7 +529,7 @@
|
|||||||
if (msg.sender === 'system') {
|
if (msg.sender === 'system') {
|
||||||
return \`
|
return \`
|
||||||
<div class="flex justify-center">
|
<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)}
|
\${escapeHtml(msg.text)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -406,10 +538,10 @@
|
|||||||
return \`
|
return \`
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<div class="max-w-[85%] sm:max-w-xs">
|
<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)}
|
\${escapeHtml(msg.text)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
@@ -417,11 +549,10 @@
|
|||||||
return \`
|
return \`
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<div class="max-w-[85%] sm:max-w-xs">
|
<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 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">
|
||||||
<div class="text-xs font-semibold" style="color: \${CONFIG.primaryColor}">\${CONFIG.brandName}</div>
|
|
||||||
\${escapeHtml(msg.text)}
|
\${escapeHtml(msg.text)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
@@ -430,21 +561,21 @@
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t bg-white p-3 sm:p-4 pb-safe">
|
<div class="p-3 sm:p-3.5 sm:mb-2 pb-6 flex-shrink-0 safe-area-bottom mobile-input-container">
|
||||||
<div class="flex gap-2">
|
<div class="glass-input rounded-xl p-2 flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
id="nostr-message-input"
|
id="nostr-message-input"
|
||||||
type="text"
|
type="text"
|
||||||
value="\${escapeHtml(state.inputMessage)}"
|
value="\${escapeHtml(state.inputMessage)}"
|
||||||
placeholder="Type your message..."
|
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"
|
class="flex-1 bg-transparent px-2 py-1.5 focus:outline-none text-sm sm:text-base text-white placeholder-white/60"
|
||||||
style="focus:ring-color: \${CONFIG.primaryColor}"
|
|
||||||
\${!state.connected ? 'disabled' : ''}
|
\${!state.connected ? 'disabled' : ''}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onclick="window.NostrChat.send()"
|
onclick="window.NostrChat.send()"
|
||||||
\${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
\${!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"
|
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">
|
<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);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only auto-focus on desktop
|
||||||
|
if (window.innerWidth >= 640) {
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expose global API
|
// Expose global API
|
||||||
window.NostrChat = {
|
window.NostrChat = {
|
||||||
open: async () => {
|
open: async () => {
|
||||||
state.isOpen = true;
|
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();
|
render();
|
||||||
if (state.relays.length === 0) {
|
if (state.relays.length === 0) {
|
||||||
await init();
|
await init();
|
||||||
@@ -496,6 +635,9 @@
|
|||||||
},
|
},
|
||||||
close: () => {
|
close: () => {
|
||||||
state.isOpen = false;
|
state.isOpen = false;
|
||||||
|
// Restore body scroll on mobile
|
||||||
|
document.documentElement.classList.remove('chat-open');
|
||||||
|
document.body.classList.remove('chat-open');
|
||||||
render();
|
render();
|
||||||
},
|
},
|
||||||
send: sendMessage
|
send: sendMessage
|
||||||
@@ -508,7 +650,6 @@
|
|||||||
document.body.appendChild(widgetScript);
|
document.body.appendChild(widgetScript);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* fill-range <https://github.com/jonschlinkert/fill-range>
|
* 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
|
* 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>
|
|
||||||
|
|
||||||
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>
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
*
|
||||||
|
* 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() {
|
(function() {
|
||||||
@@ -67,22 +28,106 @@ EMBED IT WITH:
|
|||||||
'wss://relay.btcforplebs.com'
|
'wss://relay.btcforplebs.com'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Inject Tailwind CSS
|
// Add viewport meta tag for mobile optimization
|
||||||
const tailwindLink = document.createElement('link');
|
let viewportMeta = document.querySelector('meta[name="viewport"]');
|
||||||
tailwindLink.href = 'https://cdn.tailwindcss.com';
|
if (!viewportMeta) {
|
||||||
tailwindLink.rel = 'stylesheet';
|
viewportMeta = document.createElement('meta');
|
||||||
document.head.appendChild(tailwindLink);
|
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');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.safe-area-bottom {
|
.safe-area-bottom {
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: max(env(safe-area-inset-bottom), 1rem);
|
||||||
}
|
}
|
||||||
#nostr-chat-widget-root > div {
|
#nostr-chat-widget-root > div {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
z-index: 99999 !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);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
@@ -154,14 +199,26 @@ EMBED IT WITH:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
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.myPrivKey = getSessionKey();
|
||||||
state.myPubKey = getPublicKey(state.myPrivKey);
|
state.myPubKey = getPublicKey(state.myPrivKey);
|
||||||
state.sessionId = state.myPubKey.substring(0, 8);
|
state.sessionId = state.myPubKey.substring(0, 8);
|
||||||
|
|
||||||
console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey));
|
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) => {
|
const relayPromises = CONFIG.relays.map(async (url) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(\`Attempting: \${url}\`);
|
||||||
const relay = relayInit(url);
|
const relay = relayInit(url);
|
||||||
|
|
||||||
relay.on('connect', () => {
|
relay.on('connect', () => {
|
||||||
@@ -171,6 +228,11 @@ EMBED IT WITH:
|
|||||||
|
|
||||||
relay.on('disconnect', () => {
|
relay.on('disconnect', () => {
|
||||||
console.log(\`✗ Disconnected from \${url}\`);
|
console.log(\`✗ Disconnected from \${url}\`);
|
||||||
|
checkConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
relay.on('error', (err) => {
|
||||||
|
console.error(\`❌ Relay error \${url}:\`, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
await relay.connect();
|
await relay.connect();
|
||||||
@@ -183,13 +245,13 @@ EMBED IT WITH:
|
|||||||
|
|
||||||
state.relays = (await Promise.all(relayPromises)).filter(r => r !== null);
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(\`✓ Connected to \${state.relays.length}/\${CONFIG.relays.length} relays\`);
|
|
||||||
|
|
||||||
subscribeToReplies();
|
subscribeToReplies();
|
||||||
loadPreviousMessages();
|
loadPreviousMessages();
|
||||||
|
|
||||||
@@ -278,8 +340,24 @@ EMBED IT WITH:
|
|||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
if (!state.inputMessage.trim()) return;
|
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;
|
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 {
|
try {
|
||||||
console.log('🔐 Encrypting and sending...');
|
console.log('🔐 Encrypting and sending...');
|
||||||
@@ -302,37 +380,48 @@ EMBED IT WITH:
|
|||||||
event.sig = signEvent(event, state.myPrivKey);
|
event.sig = signEvent(event, state.myPrivKey);
|
||||||
|
|
||||||
let published = 0;
|
let published = 0;
|
||||||
for (const relay of state.relays) {
|
const publishPromises = state.relays.map(async (relay) => {
|
||||||
try {
|
try {
|
||||||
await relay.publish(event);
|
await relay.publish(event);
|
||||||
published++;
|
published++;
|
||||||
console.log(\`✓ Published to \${relay.url}\`);
|
console.log(\`✓ Published to \${relay.url}\`);
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
console.error(\`✗ Failed: \${relay.url}:\`, err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
await Promise.all(publishPromises);
|
||||||
|
|
||||||
if (published === 0) {
|
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');
|
addMessage('system', '⚠️ Failed to send - no relay connections');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
console.log(\`✓ Published to \${published}/\${state.relays.length} relays\`);
|
||||||
|
|
||||||
const message = {
|
// Update temp message with real ID
|
||||||
id: event.id,
|
const msgIndex = state.messages.findIndex(m => m.id === tempMessage.id);
|
||||||
text: messageText,
|
if (msgIndex !== -1) {
|
||||||
sender: 'user',
|
state.messages[msgIndex].id = event.id;
|
||||||
timestamp: new Date().toISOString()
|
}
|
||||||
};
|
saveMessages();
|
||||||
|
|
||||||
addMessage('user', messageText, message);
|
|
||||||
state.inputMessage = '';
|
|
||||||
render();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending:', 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(() => {
|
setTimeout(() => {
|
||||||
const container = document.getElementById('nostr-messages');
|
const container = document.getElementById('nostr-messages');
|
||||||
if (container) {
|
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;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,16 +491,16 @@ EMBED IT WITH:
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = \`
|
container.innerHTML = \`
|
||||||
<div class="fixed inset-4 sm:inset-auto sm:bottom-6 sm:right-6 z-[99999]">
|
<div class="fixed inset-0 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="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-4 sm:p-5">
|
<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 justify-between items-center">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 class="font-bold text-base sm:text-lg">\${CONFIG.brandName}</h3>
|
<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>
|
<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">
|
<span class="text-xs text-white/80 truncate">
|
||||||
\${state.connected ? \`P2P E2EE • \${state.relays.length} relays\` : 'Connecting...'}
|
\${state.connected ? \`Encrypted • \${state.relays.length} relays\` : 'Connecting...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,9 +517,9 @@ EMBED IT WITH:
|
|||||||
</div>
|
</div>
|
||||||
</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 ? \`
|
\${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">
|
<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>
|
<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>
|
</svg>
|
||||||
@@ -435,7 +529,7 @@ EMBED IT WITH:
|
|||||||
if (msg.sender === 'system') {
|
if (msg.sender === 'system') {
|
||||||
return \`
|
return \`
|
||||||
<div class="flex justify-center">
|
<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)}
|
\${escapeHtml(msg.text)}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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)}
|
\${escapeHtml(msg.text)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
@@ -455,11 +549,10 @@ EMBED IT WITH:
|
|||||||
return \`
|
return \`
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<div class="max-w-[85%] sm:max-w-xs">
|
<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 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">
|
||||||
<div class="text-xs font-semibold" style="color: \${CONFIG.primaryColor}">\${CONFIG.brandName}</div>
|
|
||||||
\${escapeHtml(msg.text)}
|
\${escapeHtml(msg.text)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
@@ -468,24 +561,21 @@ EMBED IT WITH:
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t bg-white p-3 sm:p-4 safe-area-bottom">
|
<div class="p-3 sm:p-3.5 sm:mb-2 pb-6 flex-shrink-0 safe-area-bottom mobile-input-container">
|
||||||
<div class="flex gap-2">
|
<div class="glass-input rounded-xl p-2 flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
id="nostr-message-input"
|
id="nostr-message-input"
|
||||||
type="text"
|
type="text"
|
||||||
value="\${escapeHtml(state.inputMessage)}"
|
value="\${escapeHtml(state.inputMessage)}"
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
style="border-color: #d1d5db;"
|
class="flex-1 bg-transparent px-2 py-1.5 focus:outline-none text-sm sm:text-base text-white placeholder-white/60"
|
||||||
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';"
|
|
||||||
\${!state.connected ? 'disabled' : ''}
|
\${!state.connected ? 'disabled' : ''}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onclick="window.NostrChat.send()"
|
onclick="window.NostrChat.send()"
|
||||||
\${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
\${!state.connected || !state.inputMessage.trim() ? 'disabled' : ''}
|
||||||
style="background: linear-gradient(to bottom right, \${CONFIG.primaryColor}, \${CONFIG.secondaryColor});"
|
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"
|
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">
|
<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);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only auto-focus on desktop
|
||||||
|
if (window.innerWidth >= 640) {
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Expose global API
|
// Expose global API
|
||||||
window.NostrChat = {
|
window.NostrChat = {
|
||||||
open: async () => {
|
open: async () => {
|
||||||
state.isOpen = true;
|
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();
|
render();
|
||||||
if (state.relays.length === 0) {
|
if (state.relays.length === 0) {
|
||||||
await init();
|
await init();
|
||||||
@@ -537,6 +635,9 @@ EMBED IT WITH:
|
|||||||
},
|
},
|
||||||
close: () => {
|
close: () => {
|
||||||
state.isOpen = false;
|
state.isOpen = false;
|
||||||
|
// Restore body scroll on mobile
|
||||||
|
document.documentElement.classList.remove('chat-open');
|
||||||
|
document.body.classList.remove('chat-open');
|
||||||
render();
|
render();
|
||||||
},
|
},
|
||||||
send: sendMessage
|
send: sendMessage
|
||||||
@@ -549,7 +650,6 @@ EMBED IT WITH:
|
|||||||
document.body.appendChild(widgetScript);
|
document.body.appendChild(widgetScript);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* fill-range <https://github.com/jonschlinkert/fill-range>
|
* 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>
|
<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>
|
</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-nostr-pubkey="75462f4dece4fbde54a535cfa09eb0d329bda090a9c2f9ed6b5f9d1d2fb6c15b"
|
||||||
data-brand-name="Chat with BTCforPlebs"
|
data-brand-name="Chat with BTCforPlebs"
|
||||||
data-color="#8e30eb"
|
data-color="#8e30eb"
|
||||||
|
|||||||
Reference in New Issue
Block a user