diff --git a/.DS_Store b/.DS_Store index c6b0767..0d97aba 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 3aee0a2..68ffceb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ BTCforPlebs Website Videos/* node_modules/* package-lock.json public/assets/data/link-status.json +public/local.html +public/nostr-chat-widget-mini.js +public/nostr-chat-widget-mini.js diff --git a/public/nostr-chat-widget-WIP.js b/public/nostr-chat-widget-WIP.js new file mode 100644 index 0000000..aa27942 --- /dev/null +++ b/public/nostr-chat-widget-WIP.js @@ -0,0 +1,704 @@ +/** + * Nostr Chat Widget - Embeddable Version (Glassmorphism Design) + * + * EMBED IT WITH: + * + */ + + +(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'; + 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 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); + } + .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; + } + } + `; + 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) { + // Smooth scroll on desktop, instant on mobile for better keyboard handling + if (window.innerWidth >= 640) { + container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); + } else { + container.scrollTop = container.scrollHeight; + } + } + }, 100); + } + + 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 = \` +
h?l[c][f]=s+1:n.charAt(c-1)===i.charAt(f-1)?l[c][f]=l[c-1][f-1]:l[c][f]=Math.min(l[c-1][f-1]+1,Math.min(l[c][f-1]+1,l[c-1][f]+1)),l[c][f]{u();function Xg(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Zg(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Zg(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Xg(r[i],e)+t;return t}return Xg(r,e)}Jg.exports=Zg});var ry=x((lq,ty)=>{u();var Cs="-".charCodeAt(0),_s="+".charCodeAt(0),Fl=".".charCodeAt(0),j2="e".charCodeAt(0),z2="E".charCodeAt(0);function U2(r){var e=r.charCodeAt(0),t;if(e===_s||e===Cs){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===Fl&&i>=48&&i<=57}return e===Fl?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}ty.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!U2(r))return!1;for(i=r.charCodeAt(e),(i===_s||i===Cs)&&e++;e{u();function Gy(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Qy(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Qy(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Gy(r[i],e)+t;return t}return Gy(r,e)}Yy.exports=Qy});var Zy=x((o$,Xy)=>{u();var $s="-".charCodeAt(0),Ls="+".charCodeAt(0),su=".".charCodeAt(0),vO="e".charCodeAt(0),xO="E".charCodeAt(0);function kO(r){var e=r.charCodeAt(0),t;if(e===Ls||e===$s){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===su&&i>=48&&i<=57}return e===su?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Xy.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!kO(r))return!1;for(i=r.charCodeAt(e),(i===Ls||i===$s)&&e++;e\${CONFIG.brandName}
-
+
${CONFIG.brandName}
+