diff --git a/README.md b/README.md index fd25893..facfd4c 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,30 @@ Tides is a messaging application built as Chrome extension that enables secure and private communication using the Nostr protocol. -

- - - -

- - ## Features - Secure login via: - - NIP-07 browser extension (recommended) + - NIP-07 browser extension - Chrome storage - Manual private key (nsec) - Real-time encrypted messaging -- Contact management with profile pictures and usernames -- Message notifications with sound -- Media sharing support (images, videos, GIFs) +- Media link sharing and preview support: + - Images (PNG, JPG, WEBP, GIF) + - Videos (MP4, WEBM) + - YouTube videos + - Twitter/X posts + - Nostr notes and profiles - Emoji picker - Link previews - Multi-relay support +- Zaps support +- Search history for contacts +- Integrated Noderunners Radio stream + ## Installation -1. Download the latest release from the releases page +1. Download the latest release (v1.1.0) from the releases page 2. Install in Chrome: - Open Chrome and navigate to `chrome://extensions` @@ -37,7 +37,7 @@ Tides is a messaging application built as Chrome extension that enables secure a ## Login Methods -1. **NIP-07 Extension (Recommended)** +1. **NIP-07 Extension** - Install a Nostr signer extension (like nos2x or Alby) - Click "Login with Extension" in Tides @@ -57,13 +57,18 @@ Built using: - Chrome Storage API for data persistence - Web Notifications API - Giphy API for GIF support +- WebLN for Lightning Network integration Supports NIPs: - NIP-01: Basic protocol - NIP-04: Encrypted Direct Messages - NIP-05: DNS Identifiers - NIP-07: Browser Extension +- NIP-19: bech32-encoded entities +- NIP-21: nostr: URL scheme +- NIP-25: Reactions - NIP-44: Versioned Encryption +- NIP-57: Lightning Zaps - NIP-89: Application Handlers ## Privacy & Security @@ -73,6 +78,4 @@ Supports NIPs: - Private keys never leave your device - Open source and auditable -## License -MIT License - See LICENSE file for details diff --git a/lib/emoji-picker.js b/lib/emoji-picker.js new file mode 100644 index 0000000..c3e09de --- /dev/null +++ b/lib/emoji-picker.js @@ -0,0 +1,26 @@ +// Basic emoji picker implementation +class EmojiPicker { + constructor(options) { + this.onSelect = options.onSelect; + this.emojis = ['😊', '😂', '❤️', '👍', '😎', '🎉', '🔥', '✨']; + } + + togglePicker(triggerElement) { + const picker = document.createElement('div'); + picker.className = 'emoji-picker'; + picker.innerHTML = this.emojis.map(emoji => + `${emoji}` + ).join(''); + + picker.addEventListener('click', (e) => { + if (e.target.classList.contains('emoji-option')) { + this.onSelect(e.target.textContent); + picker.remove(); + } + }); + + triggerElement.parentNode.appendChild(picker); + } +} + +self.EmojiPicker = EmojiPicker; diff --git a/lib/qrcode.min.js b/lib/qrcode.min.js new file mode 100644 index 0000000..53e720c --- /dev/null +++ b/lib/qrcode.min.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using Terser v3.14.1. + * Original file: /npm/qrcode-generator@1.4.4/qrcode.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +var qrcode=function(){var t=function(t,r){var e=t,n=f[r],o=null,i=0,a=null,u=[],c={},g=function(t,r){o=function(t){for(var r=new Array(t),e=0;e=7&&v(t),null==a&&(a=y(e,n,u)),w(a,r)},l=function(t,r){for(var e=-1;e<=7;e+=1)if(!(t+e<=-1||i<=t+e))for(var n=-1;n<=7;n+=1)r+n<=-1||i<=r+n||(o[t+e][r+n]=0<=e&&e<=6&&(0==n||6==n)||0<=n&&n<=6&&(0==e||6==e)||2<=e&&e<=4&&2<=n&&n<=4)},h=function(){for(var t=8;t>n&1);o[Math.floor(n/3)][n%3+i-8-3]=a}for(n=0;n<18;n+=1){a=!t&&1==(r>>n&1);o[n%3+i-8-3][Math.floor(n/3)]=a}},d=function(t,r){for(var e=n<<3|r,a=p.getBCHTypeInfo(e),u=0;u<15;u+=1){var f=!t&&1==(a>>u&1);u<6?o[u][8]=f:u<8?o[u+1][8]=f:o[i-15+u][8]=f}for(u=0;u<15;u+=1){f=!t&&1==(a>>u&1);u<8?o[8][i-u-1]=f:u<9?o[8][15-u-1+1]=f:o[8][15-u-1]=f}o[i-8][8]=!t},w=function(t,r){for(var e=-1,n=i-1,a=7,u=0,f=p.getMaskFunction(r),c=i-1;c>0;c-=2)for(6==c&&(c-=1);;){for(var g=0;g<2;g+=1)if(null==o[n][c-g]){var l=!1;u>>a&1)),f(n,c-g)&&(l=!l),o[n][c-g]=l,-1==(a-=1)&&(u+=1,a=7)}if((n+=e)<0||i<=n){n-=e,e=-e;break}}},y=function(t,r,e){for(var n=C.getRSBlocks(t,r),o=k(),i=0;i8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u=0?h.getAt(s):0}}var v=0;for(g=0;gn)&&(t=n,r=e)}return r}())},c.createTableTag=function(t,r){t=t||2;var e="";e+='',e+="";for(var n=0;n";for(var o=0;o';e+=""}return e+="",e+="
"},c.createSvgTag=function(t,r,e,n){var o={};"object"==typeof arguments[0]&&(t=(o=arguments[0]).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,f,g=c.getModuleCount()*t+2*r,l="";for(f="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",l+=''+m(n.text)+"":"",l+=e.text?''+m(e.text)+"":"",l+='',l+='":r+=">";break;case"&":r+="&";break;case'"':r+=""";break;default:r+=n}}return r};return c.createASCII=function(t,r){if((t=t||1)<2)return function(t){t=void 0===t?2:t;var r,e,n,o,i,a=1*c.getModuleCount()+2*t,u=t,f=a-t,g={"██":"█","█ ":"▀"," █":"▄"," ":" "},l={"██":"▀","█ ":"▀"," █":" "," ":" "},h="";for(r=0;r=f?l[i]:g[i];h+="\n"}return a%2&&t>0?h.substring(0,h.length-a-1)+Array(a+1).join("▀"):h.substring(0,h.length-1)}(r);t-=1,r=void 0===r?2*t:r;var e,n,o,i,a=c.getModuleCount()*t+2*r,u=r,f=a-r,g=Array(t+1).join("██"),l=Array(t+1).join(" "),h="",s="";for(e=0;e>>8),r.push(255&a)):r.push(n)}}return r}};var r,e,n,o=1,i=2,a=4,u=8,f={L:1,M:0,Q:3,H:2},c=0,g=1,l=2,h=3,s=4,v=5,d=6,w=7,p=(r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],n=function(t){for(var r=0;0!=t;)r+=1,t>>>=1;return r},(e={}).getBCHTypeInfo=function(t){for(var r=t<<10;n(r)-n(1335)>=0;)r^=1335<=0;)r^=7973<5&&(e+=3+i-5)}for(n=0;n=256;)r-=255;return t[r]}};return n}();function B(t,r){if(void 0===t.length)throw t.length+"/"+r;var e=function(){for(var e=0;e>>7-r%8&1)},put:function(t,r){for(var n=0;n>>r-n-1&1))},getLengthInBits:function(){return r},putBit:function(e){var n=Math.floor(r/8);t.length<=n&&t.push(0),e&&(t[n]|=128>>>r%8),r+=1}};return e},A=function(t){var r=o,e=t,n={getMode:function(){return r},getLength:function(t){return e.length},write:function(t){for(var r=e,n=0;n+2>>8&255)+(255&n),t.put(n,13),e+=2}if(e>>8)},writeBytes:function(t,e,n){e=e||0,n=n||t.length;for(var o=0;o0&&(r+=","),r+=t[e];return r+="]"}};return r},L=function(t){var r=t,e=0,n=0,o=0,i={read:function(){for(;o<8;){if(e>=r.length){if(0==o)return-1;throw"unexpected end of file./"+o}var t=r.charAt(e);if(e+=1,"="==t)return o=0,-1;t.match(/^\s$/)||(n=n<<6|a(t.charCodeAt(0)),o+=6)}var i=n>>>o-8&255;return o-=8,i}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return i},D=function(t,r,e){for(var n=function(t,r){var e=t,n=r,o=new Array(t*r),i={setPixel:function(t,r,n){o[r*e+t]=n},write:function(t){t.writeString("GIF87a"),t.writeShort(e),t.writeShort(n),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(e),t.writeShort(n),t.writeByte(0);var r=a(2);t.writeByte(2);for(var o=0;r.length-o>255;)t.writeByte(255),t.writeBytes(r,o,255),o+=255;t.writeByte(r.length-o),t.writeBytes(r,o,r.length-o),t.writeByte(0),t.writeString(";")}},a=function(t){for(var r=1<>>r!=0)throw"length over";for(;c+r>=8;)f.writeByte(255&(t<>>=8-c,g=0,c=0;g|=t<0&&f.writeByte(g)}});h.write(r,n);var s=0,v=String.fromCharCode(o[s]);for(s+=1;s=6;)h(u>>>f-6),f-=6},l.flush=function(){if(f>0&&(h(u<<6-f),u=0,f=0),c%3!=0)for(var t=3-c%3,r=0;r>6,128|63&n):n<55296||n>=57344?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},function(t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports&&(module.exports=t())}(function(){return qrcode}); +//# sourceMappingURL=/sm/467f1e4dbdb6a7c0f745f3affd92b4084db234a4e15305cb31384a24c2a4b457.map \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..98cf105 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1111 @@ +{ + "name": "tides", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tides", + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "emoji-picker-element": "^1.23.0", + "nostr-tools": "^1.17.0", + "qrcode-generator": "^1.4.4" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.2", + "@rollup/plugin-node-resolve": "^15.1.0", + "@rollup/plugin-terser": "^0.4.3", + "@types/chrome": "^0.0.260", + "rollup": "^2.79.1", + "rollup-plugin-copy": "^3.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/ciphers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", + "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.260", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.260.tgz", + "integrity": "sha512-lX6QpgfsZRTDpNcCJ+3vzfFnFXq9bScFRTlfhbK5oecSAjamsno+ejFTCbNtc5O/TPnVK9Tja/PyecvWQe0F2w==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-picker-element": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.23.0.tgz", + "integrity": "sha512-eITYquXyUoaTkpxJxEMIzCO6y3xpFxLvLuIRbnG+fL5u0aicHzKFvjSScrOSEwqg+2LWUlDqu08vqBFvtBOAeQ==" + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/globby/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nostr-tools": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", + "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", + "dependencies": { + "@noble/ciphers": "0.2.0", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/qrcode-generator": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", + "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json index dd6fa53..8448084 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tides", - "version": "1.0.0", + "version": "1.0.1", "description": "A Nostr-based Messenger", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -47,7 +47,8 @@ } }, "scripts": { - "build": "rollup -c && cp src/icons/*.png dist/icons/", + "prebuild": "mkdir -p dist/lib", + "build": "rollup -c && cp src/icons/*.png dist/icons/ && cp -r lib/* dist/lib/", "start": "echo \"Starting...\"", "clean": "rm -rf dist" }, @@ -57,7 +58,8 @@ "license": "ISC", "dependencies": { "emoji-picker-element": "^1.23.0", - "nostr-tools": "^1.17.0" + "nostr-tools": "^1.17.0", + "qrcode-generator": "^1.4.4" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.2", diff --git a/rollup.config.js b/rollup.config.js index 6bdc187..a8b4929 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,46 +3,49 @@ import commonjs from '@rollup/plugin-commonjs' import terser from '@rollup/plugin-terser' import copy from 'rollup-plugin-copy' -const createConfig = (input, output, name) => ({ - input, - output: { - file: `dist/${output}`, - format: 'iife', - name, - sourcemap: true, - globals: { - 'nostr-tools': 'NostrTools' - } - }, - external: ['nostr-tools'], - plugins: [ - resolve({ - browser: true, - preferBuiltins: false, - mainFields: ['browser', 'module', 'main'] - }), - commonjs({ - include: /node_modules/, - transformMixedEsModules: true - }), - terser(), - copy({ - targets: [ - { src: 'src/popup.html', dest: 'dist' }, - { src: 'src/style.css', dest: 'dist' }, - { src: 'src/manifest.json', dest: 'dist' }, - { src: 'src/sounds/*', dest: 'dist/sounds' }, - { src: 'src/icons/Logo.png', dest: 'dist/icons' }, - { src: 'src/icons/default-avatar.png', dest: 'dist/icons' }, - { - src: 'node_modules/nostr-tools/lib/nostr.bundle.js', - dest: 'dist/lib', - rename: 'nostr-tools.js' - } - ] - }) - ] -}); +function createConfig(input, output, name) { + return { + input, + output: { + file: `dist/${output}`, + format: 'iife', + name, + globals: { + qrcode: 'qrcode', + 'nostr-tools': 'NostrTools' + } + }, + external: ['qrcode', 'nostr-tools'], + plugins: [ + resolve({ + browser: true, + preferBuiltins: false, + mainFields: ['browser', 'module', 'main'] + }), + commonjs({ + include: /node_modules/, + transformMixedEsModules: true + }), + terser(), + copy({ + targets: [ + { src: 'src/popup.html', dest: 'dist' }, + { src: 'src/style.css', dest: 'dist' }, + { src: 'src/manifest.json', dest: 'dist' }, + { src: 'src/sounds/*', dest: 'dist/sounds' }, + { src: 'src/icons/Logo.png', dest: 'dist/icons' }, + { src: 'src/icons/default-avatar.png', dest: 'dist/icons' }, + { + src: 'node_modules/nostr-tools/lib/nostr.bundle.js', + dest: 'dist/lib', + rename: 'nostr-tools.js' + }, + { src: 'node_modules/qrcode-generator/qrcode.min.js', dest: 'dist/lib' } + ] + }) + ] + }; +} export default [ createConfig('src/background.js', 'background.js', 'Background'), diff --git a/src/auth.js b/src/auth.js index 871db28..53c3c60 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,5 +1,5 @@ import { nostrCore, pool, RELAYS, shortenIdentifier } from './shared.js'; -import { validateEvent, getStoredCredentials } from './utils.js'; +import { validateEvent, getStoredCredentials, soundManager } from './utils.js'; import { storeMetadata } from './userMetadata.js'; class Auth { @@ -30,6 +30,7 @@ class Auth { if (user) { await this.storeCredentials(user); + soundManager.play('login'); } return user; @@ -56,7 +57,6 @@ class Auth { }; await this.storeCredentials(credentials); - soundManager.play('login'); return credentials; } catch (error) { @@ -85,7 +85,6 @@ class Auth { }; await this.storeCredentials(credentials); - soundManager.play('login'); return credentials; } catch (error) { diff --git a/src/background.js b/src/background.js index 2e3263e..58d6ed5 100644 --- a/src/background.js +++ b/src/background.js @@ -1,109 +1,401 @@ -// Core dependencies -import { pool, relayPool, RELAYS, pubkeyToNpub, nostrCore } from './shared.js'; -import { validateEvent, toLowerCaseHex, soundManager } from './utils.js'; -import { auth } from './auth.js'; -import { messageManager, sendMessage, receiveMessage, fetchMessages } from './messages.js'; -import { fetchContacts, setContacts, processContactEvent } from './contact.js'; -import { getUserMetadata, getDisplayName, getAvatarUrl, storeMetadata } from './userMetadata.js'; -import { publishAppHandlerEvent } from './nip89.js'; - -// Move to top level (after imports): -let currentSubscription = null; - -// Register service worker -self.addEventListener('install', async (event) => { - console.log('Service Worker installing.'); - event.waitUntil(self.skipWaiting()); -}); - -self.addEventListener('activate', async (event) => { - console.log('Service Worker activated.'); - event.waitUntil(Promise.all([ - clients.claim(), - auth.init() - ])); -}); - -self.addEventListener('message', async (event) => { - const { type, data } = event.data; - - if (type === 'LOGIN_SUCCESS') { - const user = await auth.getCurrentUser(); - if (user) { +var Background = (function(NostrTools) { + 'use strict'; + + // Core dependencies initialization from NostrTools + const nostrCore = { + nip19: NostrTools.nip19, + getPublicKey: NostrTools.getPublicKey, + getEventHash: NostrTools.getEventHash, + getSignature: NostrTools.getSignature + }; + + const pool = new NostrTools.SimplePool(); + const RELAYS = [ + "wss://relay.damus.io", + "wss://relay.nostr.band", + "wss://nos.lol", + "wss://relay.snort.social", + "wss://nostr.wine" + ]; + + // Utils + const validateEvent = (event) => { + try { + return event && 'object' === typeof event && event.id && event.pubkey && + event.created_at && event.kind && event.content; + } catch (error) { + console.error('Event validation failed:', error); + return null; + } + }; + + const soundManager = new class { + constructor() { + this.sounds = new Map([ + ['login', chrome.runtime.getURL('sounds/login.mp3')], + ['message', chrome.runtime.getURL('sounds/icq_message.mp3')] + ]); + this.played = new Set(); + this.enabled = true; + } + + async play(type, once = false) { + if (!this.enabled || (once && this.played.has(type))) return; + const soundUrl = this.sounds.get(type); + if (soundUrl) { + try { + const audio = new Audio(soundUrl); + if (type === 'login') { + audio.volume = 0.1; + } + await audio.play(); + if (once) this.played.add(type); + } catch (error) { + console.error(`Error playing ${type} sound:`, error); + } + } + } + }; + + // State management + let currentSubscription = null; + const contacts = new Map(); + const messageManager = new class { + constructor() { + this.subscriptions = new Map(); + this.messageCache = new Map(); + } + + async handleIncomingMessage(event) { + const decryptedContent = await this.decryptMessage(event); + if (decryptedContent) { + chrome.runtime.sendMessage({ + type: 'NEW_MESSAGE', + data: { + id: event.id, + pubkey: event.pubkey, + content: decryptedContent, + created_at: event.created_at + } + }); + soundManager.play('message'); + } + } + + async decryptMessage(event) { + // Your existing decryption logic + } + }; + + // Add auth class before Service Worker Event Listeners + const auth = new class { + constructor() { + this.currentUser = null; + } + + async init() { + const credentials = await this.getStoredCredentials(); + if (credentials) { + this.currentUser = credentials; + return credentials; + } + return null; + } + + async getCurrentUser() { + return this.currentUser || await this.getStoredCredentials(); + } + + async getStoredCredentials() { try { - await relayPool.ensureConnection(); + const { currentUser } = await chrome.storage.local.get('currentUser'); + return currentUser || null; + } catch (error) { + console.error('Failed to get stored credentials:', error); + return null; + } + } + + async login(method, key) { + try { + let credentials; + if (method === 'NIP-07') { + credentials = await this.loginWithNIP07(); + } else if (method === 'NSEC') { + credentials = await this.loginWithNSEC(key); + } else { + throw new Error('Invalid login method'); + } + + if (credentials) { + await this.storeCredentials(credentials); + soundManager.play('login'); + } + return credentials; + } catch (error) { + console.error('Login failed:', error); + throw error; + } + } + + async loginWithNIP07() { + try { + // Check if NIP-07 extension exists in extension context + if (typeof window?.nostr === 'undefined') { + throw new Error('No Nostr extension found. Please install Alby or nos2x.'); + } + + // Test if we can actually get permissions + await window.nostr.enable(); + + // Get public key + const pubkey = await window.nostr.getPublicKey(); + + // Verify we can sign (this confirms the extension is working) + const testEvent = { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: 'test' + }; - if (currentSubscription) { - currentSubscription.unsub(); + try { + await window.nostr.signEvent(testEvent); + } catch (e) { + throw new Error('Nostr extension cannot sign events. Please check its permissions.'); } - // First fetch existing data - const contacts = await fetchContacts(user.pubkey); - if (contacts.length > 0) { - setContacts(contacts); + const npub = nostrCore.nip19.npubEncode(pubkey); + return { + type: 'NIP-07', + pubkey: pubkey.toLowerCase(), + npub, + displayId: npub.slice(0, 8) + '...' + npub.slice(-4) + }; + } catch (error) { + console.error('NIP-07 login failed:', error); + throw error; + } + } + + async loginWithNSEC(nsec) { + try { + const { type, data: privkey } = nostrCore.nip19.decode(nsec); + if (type !== 'nsec') throw new Error('Invalid nsec format'); + const pubkey = nostrCore.getPublicKey(privkey); + const npub = nostrCore.nip19.npubEncode(pubkey); + return { + type: 'NSEC', + pubkey, + privkey, + npub, + displayId: npub.slice(0, 8) + '...' + npub.slice(-4) + }; + } catch (error) { + throw error; + } + } + + async storeCredentials(credentials) { + if (!credentials?.pubkey) throw new Error('Invalid credentials format'); + this.currentUser = credentials; + await chrome.storage.local.set({ + currentUser: credentials, + [`credentials:${credentials.pubkey}`]: credentials + }); + return credentials; + } + }; + + // Service Worker Event Listeners + self.addEventListener('install', async (event) => { + console.log('Service Worker installing.'); + event.waitUntil(self.skipWaiting()); + }); + + self.addEventListener('activate', async (event) => { + console.log('Service Worker activated.'); + event.waitUntil(Promise.all([ + clients.claim(), + auth.init() + ])); + }); + + self.addEventListener('message', async (event) => { + const { type, data } = event.data; + + if (type === 'LOGIN_SUCCESS') { + const user = await auth.getCurrentUser(); + if (user) { + try { + await relayPool.ensureConnection(); + + if (currentSubscription) { + currentSubscription.unsub(); + } + + // First fetch existing data + const contacts = await fetchContacts(user.pubkey); + if (contacts.length > 0) { + setContacts(contacts); + chrome.runtime.sendMessage({ + type: 'CONTACTS_UPDATED', + data: contacts + }); + } + + // Then set up live subscriptions + currentSubscription = pool.sub( + RELAYS.map(relay => ({ + relay, + filter: [ + { kinds: [3], authors: [user.pubkey] }, + { kinds: [0], authors: [user.pubkey] }, + { kinds: [4], '#p': [user.pubkey] }, + { kinds: [9735], '#p': [user.pubkey] }, + { kinds: [42], '#e': user.channelIds }, + { kinds: [30311], '#p': [user.pubkey] } + ] + })) + ); + + currentSubscription.on('event', async (event) => { + if (validateEvent(event)) { + console.log('Received event:', event); + if (event.kind === 0) { + const metadata = JSON.parse(event.content); + await storeMetadata(event.pubkey, metadata); + } else if (event.kind === 3) { + const contacts = await processContactEvent(event); + setContacts(contacts); + chrome.runtime.sendMessage({ + type: 'CONTACTS_UPDATED', + data: contacts + }); + } else if (event.kind === 4) { + await messageManager.handleIncomingMessage(event); + } else if (event.kind === 9735) { + const zapAmount = event.tags.find(t => t[0] === 'amount')?.[1]; + const messageId = event.tags.find(t => t[0] === 'e')?.[1]; + if (zapAmount && messageId) { + chrome.runtime.sendMessage({ + type: 'ZAP_RECEIVED', + data: { messageId, amount: parseInt(zapAmount) } + }); + } + } else if (event.kind === 30311) { + const streamMetadata = JSON.parse(event.content); + const streamId = event.tags.find(t => t[0] === 'd')?.[1]; + if (streamId) { + const streamData = { + pubkey: event.pubkey, + displayName: streamMetadata.title || 'Unnamed Stream', + isChannel: true, + avatarUrl: streamMetadata.image || '/icons/default-avatar.png', + streamUrl: streamId, + embedUrl: `https://zap.stream/embed/${streamId}`, + about: streamMetadata.description + }; + + chrome.runtime.sendMessage({ + type: 'STREAM_UPDATED', + data: streamData + }); + } + } + } + }); + + soundManager.play('login', true); + + chrome.runtime.sendMessage({ + type: 'INIT_COMPLETE', + data: { user } + }); + } catch (error) { + console.error('Error during initialization:', error); chrome.runtime.sendMessage({ - type: 'CONTACTS_UPDATED', - data: contacts + type: 'INIT_ERROR', + error: error.message }); } + } + } + }); - // Then set up live subscriptions - currentSubscription = pool.sub( - RELAYS.map(relay => ({ - relay, - filter: [ - { kinds: [3], authors: [user.pubkey] }, - { kinds: [0], authors: [user.pubkey] }, - { kinds: [4], '#p': [user.pubkey] }, - { kinds: [42], '#e': user.channelIds } - ] - })) - ); - - currentSubscription.on('event', async (event) => { - if (validateEvent(event)) { - console.log('Received event:', event); - if (event.kind === 0) { - const metadata = JSON.parse(event.content); - await storeMetadata(event.pubkey, metadata); - } else if (event.kind === 3) { - const contacts = await processContactEvent(event); - setContacts(contacts); - chrome.runtime.sendMessage({ - type: 'CONTACTS_UPDATED', - data: contacts - }); - } else if (event.kind === 4) { - await messageManager.handleIncomingMessage(event); - } + // Add this to your message listener + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'GET_ZAP_INVOICE') { + (async () => { + try { + const { lightningAddress, amount, zapRequest } = message.data; + + let lightningUrl; + if (lightningAddress.includes('@')) { + const [name, domain] = lightningAddress.split('@'); + lightningUrl = `https://${domain}/.well-known/lnurlp/${name}`; + } else { + lightningUrl = lightningAddress; } - }); - soundManager.play('login', true); - - chrome.runtime.sendMessage({ - type: 'INIT_COMPLETE', - data: { user } - }); - } catch (error) { - console.error('Error during initialization:', error); + // Get LNURL data with proper headers + const response = await fetch(lightningUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) throw new Error('Failed to fetch lightning address info'); + const lnurlData = await response.json(); + + // Get invoice with proper headers + const callbackUrl = new URL(lnurlData.callback); + callbackUrl.search = new URLSearchParams({ + amount: amount * 1000, + nostr: JSON.stringify(zapRequest) + }).toString(); + + const invoiceResponse = await fetch(callbackUrl.toString(), { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + + if (!invoiceResponse.ok) throw new Error('Failed to generate invoice'); + const { pr: invoice } = await invoiceResponse.json(); + + sendResponse({ invoice }); + } catch (error) { + console.error('Zap invoice error:', error); + sendResponse({ error: error.message }); + } + })(); + return true; + } + }); + + // Public API + return { + updateContactStatus: function(pubkey, isOnline) { + const contact = contacts.get(pubkey); + if (contact) { + contact.isOnline = isOnline; chrome.runtime.sendMessage({ - type: 'INIT_ERROR', - error: error.message + type: 'contactStatusUpdated', + pubkey, + isOnline }); } - } - } -}); - -export function updateContactStatus(pubkey, isOnline) { - const contact = contacts.get(pubkey); - if (contact) { - contact.isOnline = isOnline; - chrome.runtime.sendMessage({ - type: 'contactStatusUpdated', - pubkey, - isOnline - }); - } -} + }, + messageManager, + soundManager, + pool, + contacts, + auth + }; + +})(NostrTools); diff --git a/src/embed.html b/src/embed.html new file mode 100644 index 0000000..e79ba13 --- /dev/null +++ b/src/embed.html @@ -0,0 +1,44 @@ + + + + Noderunners Radio + + + + + + \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 5add174..8dedcba 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,20 +1,19 @@ { "manifest_version": 3, "name": "Tides", - "version": "1.0.1", - "description": "A Nostr-based Messenger", - "permissions": ["storage", "notifications"], + "version": "1.1.0", + "description": "A Nostr Messenger For Your Chromium Browser", + "permissions": [ + "storage", + "notifications" + ], + "host_permissions": [ + "*://*/*", + "https://*.twitch.tv/*" + ], "background": { "service_worker": "background-wrapper.js" }, - "content_scripts": [{ - "matches": [""], - "js": ["lib/nostr-tools.js"] - }], - "web_accessible_resources": [{ - "resources": ["sounds/*", "icons/*", "lib/*"], - "matches": [""] - }], "action": { "default_popup": "popup.html", "default_icon": { @@ -27,5 +26,12 @@ "16": "icons/Logo.png", "48": "icons/Logo.png", "128": "icons/Logo.png" + }, + "web_accessible_resources": [{ + "resources": ["sounds/*", "icons/*", "lib/*"], + "matches": [""] + }], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'; frame-src https://www.youtube.com https://youtube.com https://*.twitter.com https://*.x.com https://*.instagram.com https://*.tiktok.com https://player.twitch.tv https://embed.twitch.tv https://*.twitch.tv https://radio.noderunners.org" } } diff --git a/src/messages.js b/src/messages.js index e714801..726de77 100644 --- a/src/messages.js +++ b/src/messages.js @@ -14,60 +14,65 @@ export class MessageManager { async fetchMessages(pubkey) { const currentUser = await auth.getCurrentUser(); if (!currentUser?.pubkey) { - throw new Error("User not authenticated"); + throw new Error('User not authenticated'); } try { await relayPool.ensureConnection(); - const relays = relayPool.getConnectedRelays(); + const connectedRelays = relayPool.getConnectedRelays(); const userPubkey = currentUser.pubkey.toLowerCase(); - const contactPubkey = pubkey.toLowerCase(); - + const targetPubkey = pubkey.toLowerCase(); + + // Create filters for both sent and received messages const filters = [ { kinds: [4], - "#p": [contactPubkey], + '#p': [targetPubkey], authors: [userPubkey] }, { kinds: [4], - "#p": [userPubkey], - authors: [contactPubkey] + '#p': [userPubkey], + authors: [targetPubkey] } ]; - if (userPubkey === contactPubkey) { + // Handle self-messages + if (userPubkey === targetPubkey) { filters.length = 0; filters.push({ kinds: [4], - "#p": [userPubkey], + '#p': [userPubkey], authors: [userPubkey] }); } - const events = await this.pool.list(relays, filters); + const events = await this.pool.list(connectedRelays, filters); + // Process and decrypt messages const messages = await Promise.all( events .filter(this.validateEvent) .sort((a, b) => a.created_at - b.created_at) - .map(async event => { + .map(async (event) => { const decrypted = await this.decryptMessage(event); - return decrypted ? { + if (!decrypted) return null; + + return { id: event.id, pubkey: event.pubkey, content: decrypted, timestamp: event.created_at * 1000, tags: event.tags - } : null; + }; }) ); return messages.filter(Boolean); - } catch (error) { - console.error("Error fetching messages:", error); - throw error; + } catch (err) { + console.error('Error fetching messages:', err); + throw err; } } @@ -75,77 +80,40 @@ export class MessageManager { try { const currentUser = await auth.getCurrentUser(); const privateKey = await auth.getPrivateKey(); - if (!privateKey) throw new Error('No private key available'); - + + if (!privateKey) { + throw new Error('No private key available'); + } + let decrypted; - const isOwnMessage = event.pubkey === currentUser.pubkey; + const isSent = event.pubkey === currentUser.pubkey; + // Get recipient pubkey from tags + const recipientPubkey = isSent + ? event.tags.find(tag => tag[0] === 'p')?.[1] + : event.pubkey; + + if (!recipientPubkey) { + throw new Error('No recipient pubkey found in tags'); + } + + // Handle different encryption methods if (privateKey === window.nostr) { decrypted = await window.nostr.nip04.decrypt( - isOwnMessage ? event.tags.find(t => t[0] === 'p')?.[1] : event.pubkey, + isSent ? recipientPubkey : event.pubkey, event.content ); } else { decrypted = await NostrTools.nip04.decrypt( privateKey, - isOwnMessage ? event.tags.find(t => t[0] === 'p')?.[1] : event.pubkey, + isSent ? recipientPubkey : event.pubkey, event.content ); } - - // First check if it's a market order - try { - const parsed = JSON.parse(decrypted); - if (parsed.items && parsed.shipping_id) { - return { - type: 'market-order', - content: parsed - }; - } - } catch {} - - // Check if content is a Giphy URL - if (decrypted.includes('giphy.com')) { - return { - type: 'media', - content: '', - mediaUrl: decrypted, - urls: [decrypted] - }; - } - - // Then check for media links - const mediaMatch = decrypted.match(/https?:\/\/[^\s<]+[^<.,:;"')\]\s](?:\.(?:jpg|jpeg|gif|png|mp4|webm|mov|ogg))/i); - const urlMatch = decrypted.match(/https?:\/\/[^\s<]+/g); - const textContent = decrypted.replace(mediaMatch?.[0] || '', '').trim(); - if (mediaMatch) { - return { - type: 'media', - content: textContent || decrypted, - mediaUrl: mediaMatch[0], - urls: urlMatch?.filter(url => url !== mediaMatch[0]) || [] - }; - } - - // Check for URLs that might need preview - if (urlMatch) { - return { - type: 'text', - content: decrypted, - urls: urlMatch, - needsPreview: true - }; - } - - // Regular text (may include emojis) - return { - type: 'text', - content: decrypted - }; - - } catch (error) { - console.error('Failed to decrypt message:', error); + return decrypted; + } catch (err) { + console.error('Failed to decrypt message:', err); return null; } } @@ -159,8 +127,8 @@ export class MessageManager { event.created_at && event.kind && event.content; - } catch (error) { - console.error('Event validation failed:', error); + } catch (err) { + console.error('Event validation failed:', err); return null; } } diff --git a/src/popup.html b/src/popup.html index 678da68..4d59fdc 100644 --- a/src/popup.html +++ b/src/popup.html @@ -17,9 +17,10 @@
- +
+
@@ -55,6 +56,7 @@
- + + diff --git a/src/popup.js b/src/popup.js index 3557c12..8a00872 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,5 +1,3 @@ -console.log('popup.js loaded'); - import { auth } from './auth.js'; import { fetchContacts, setContacts, contactManager, getContact } from './contact.js'; import { pubkeyToNpub } from './shared.js'; @@ -11,12 +9,14 @@ import { soundManager } from './utils.js'; import { shortenIdentifier } from './shared.js'; import 'emoji-picker-element'; import { searchGifs, getTrendingGifs } from './services/giphy.js'; - +import { RELAYS } from './shared.js'; +import qrcode from 'qrcode'; let currentChatPubkey = null; let emojiButtonListener; let emojiPickerListener; let hasPlayedLoginSound = false; let lastMessageTimestamp = 0; +let searchHistory = []; function initializeGifButton() { const gifButton = document.getElementById('gifButton'); @@ -100,12 +100,15 @@ function showLoginScreen() { document.getElementById('userInfo').style.visibility = 'hidden'; } -function initializeUI() { +async function initializeUI() { const loginScreen = document.getElementById('loginScreen'); const mainContainer = document.getElementById('mainContainer'); const nsecInput = document.getElementById('nsecInput'); const loginButton = document.getElementById('loginButton'); + // Load search history before initializing search input + await loadSearchHistory(); + loginScreen.style.display = 'block'; mainContainer.style.display = 'none'; nsecInput.style.display = 'none'; @@ -119,11 +122,8 @@ function initializeUI() { searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase().trim(); - - // Handle clear button visibility clearSearchButton.style.display = searchTerm ? 'block' : 'none'; - // Handle contact filtering const filteredContacts = Array.from(contactManager.contacts.values()) .filter(contact => { if (!searchTerm) return true; @@ -144,6 +144,8 @@ function initializeUI() { } document.addEventListener('DOMContentLoaded', async () => { + await loadSearchHistory(); + initializeSearchInput(); initializeUI(); initializeGifButton(); const storedUser = await auth.getStoredCredentials(); @@ -365,7 +367,7 @@ function createContactElement(contact) { element.classList.add('selected'); } - const avatarSrc = contact.avatarUrl || (contact.isChannel ? '/icons/default-channel.png' : '/icons/default-avatar.png'); + const avatarSrc = contact.avatarUrl || (contact.isChannel ? '/icons/default-avatar.png' : '/icons/default-avatar.png'); // Create elements directly instead of using innerHTML const img = document.createElement('img'); @@ -418,84 +420,59 @@ function createContactElement(contact) { return element; } -chrome.runtime.onMessage.addListener((message) => { - switch (message.type) { - case 'NEW_MESSAGE': - handleNewMessage(message.data); - break; - case 'CONTACT_UPDATED': - updateContactInList(message.data); - break; - case 'INIT_COMPLETE': - console.log('Initialization complete'); - break; - case 'INIT_ERROR': - showErrorMessage(message.error); - break; - } - return true; -}); - async function selectContact(pubkey) { await initializeChat(pubkey); } async function initializeChat(pubkey) { - const chatHeader = document.getElementById('chatHeader'); + currentChatPubkey = pubkey; const chatContainer = document.getElementById('chatContainer'); - const messageInputContainer = document.querySelector('.message-input-container'); + const chatHeader = document.getElementById('chatHeader'); - if (!pubkey) { - resetChatUI(); - return; + if (!chatContainer || !chatHeader) return; + + // Get all input elements and buttons once + const messageInput = document.getElementById('messageInput'); + const sendButton = document.getElementById('sendButton'); + const emojiButton = document.getElementById('emojiButton'); + const gifButton = document.getElementById('gifButton'); + + // Update chat header + const contact = await getContact(pubkey); + if (contact) { + updateChatHeader(chatHeader, contact); } - - currentChatPubkey = pubkey; - try { - const contact = getContact(pubkey); - if (!contact) return; + chatContainer.innerHTML = '
Loading messages...
'; + + const messages = await messageManager.fetchMessages(pubkey); + if (messages && messages.length > 0) { + await renderMessages(messages); + } else { + chatContainer.querySelector('.message-list').innerHTML = '
No messages yet
'; + } - // Handle stream/channel differently - if (contact.isChannel) { - await handleStreamChat(pubkey, chatHeader, chatContainer); - messageInputContainer.style.display = 'none'; - // Clear message input and disable buttons for streams - const messageInput = document.getElementById('messageInput'); - const sendButton = document.getElementById('sendButton'); - const emojiButton = document.getElementById('emojiButton'); - const gifButton = document.getElementById('gifButton'); - - messageInput.value = ''; - messageInput.disabled = true; - sendButton.disabled = true; - emojiButton.disabled = true; - gifButton.disabled = true; - return; - } + // Enable input for regular chats + messageInput.disabled = false; + sendButton.disabled = false; - // Regular chat handling continues... - messageInputContainer.style.display = 'flex'; - updateChatHeader(chatHeader, contact); - chatContainer.innerHTML = '
Loading messages...
'; - - const messages = await messageManager.fetchMessages(pubkey); - if (messages && messages.length > 0) { - await renderMessages(messages); - } else { - chatContainer.querySelector('.message-list').innerHTML = '
No messages yet
'; + // Add message handlers here + messageInput.addEventListener('keypress', async (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + await sendMessage(); } + }); - // Enable input for regular chats - const messageInput = document.getElementById('messageInput'); - const sendButton = document.getElementById('sendButton'); - messageInput.disabled = false; - sendButton.disabled = false; - - } catch (error) { - console.error('Failed to initialize chat:', error); - chatContainer.innerHTML = '
Failed to load messages
'; + sendButton.addEventListener('click', sendMessage); + + // Initialize emoji picker + if (emojiButton && messageInput) { + initializeEmojiPicker(emojiButton, messageInput); } + + // Initialize GIF button + initializeGifButton(); } function resetChatUI() { @@ -522,111 +499,99 @@ async function renderMessages(messages) { messageList.innerHTML = ''; const currentUser = await auth.getCurrentUser(); - messages - .sort((a, b) => a.timestamp - b.timestamp) - .forEach(message => { - const messageElement = document.createElement('div'); - const isSent = message.pubkey === currentUser?.pubkey; - messageElement.className = `message ${isSent ? 'sent' : 'received'}`; - - const bubbleElement = document.createElement('div'); - bubbleElement.className = 'message-bubble'; - - if (typeof message.content === 'string') { - bubbleElement.innerHTML = `
${linkifyText(message.content)}
`; - } else { - // Handle decrypted content object - const content = message.content; - if (content.type === 'media') { - bubbleElement.innerHTML = ` -
- -
- ${content.content ? `
${linkifyText(content.content)}
` : ''} - `; - } else { - bubbleElement.innerHTML = `
${linkifyText(content.content || '')}
`; - } + // Sort messages by timestamp + const sortedMessages = messages.sort((a, b) => a.created_at - b.created_at); + + // Create a document fragment for better performance + const fragment = document.createDocumentFragment(); + + for (const message of sortedMessages) { + const messageElement = document.createElement('div'); + const isSent = message.pubkey === currentUser?.pubkey; + messageElement.className = `message ${isSent ? 'sent' : 'received'}`; + messageElement.setAttribute('data-message-id', message.id); + + const bubbleElement = document.createElement('div'); + bubbleElement.className = 'message-bubble'; + + // Render message content (text, media, etc) + await renderMessageContent(message, bubbleElement); + + // Add zap container for received messages + if (!isSent) { + const metadata = await getUserMetadata(message.pubkey); + if (metadata?.lud16 || metadata?.lightning) { + const zapContainer = document.createElement('div'); + zapContainer.className = 'zap-container'; + zapContainer.innerHTML = ` + + ${message.zapAmount || ''} + `; + + bubbleElement.style.position = 'relative'; + + const zapButton = zapContainer.querySelector('.zap-button'); + zapButton.addEventListener('click', async (e) => { + e.stopPropagation(); + await showZapModal(message, metadata, zapContainer); + }); + + bubbleElement.appendChild(zapContainer); } - - messageElement.appendChild(bubbleElement); - messageList.appendChild(messageElement); - }); - - // Force scroll to bottom - chatContainer.scrollTop = chatContainer.scrollHeight; -} - -// Setup message input handlers -messageInput.addEventListener('keypress', async (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - await sendMessage(); - } -}); - -sendButton.addEventListener('click', sendMessage); + } -emojiButtonListener = () => { - // Remove any existing picker - const existingPicker = document.querySelector('emoji-picker'); - if (existingPicker) { - existingPicker.remove(); - return; // Toggle behavior - if picker exists, just remove it + messageElement.appendChild(bubbleElement); + fragment.appendChild(messageElement); } - const picker = document.createElement('emoji-picker'); - picker.classList.add('emoji-picker'); + // Append all messages at once + messageList.appendChild(fragment); - if (emojiPickerListener) { - picker.removeEventListener('emoji-click', emojiPickerListener); + // Call loadLinkPreviews after messages are rendered + await loadLinkPreviews(); + + // Scroll to bottom + const lastMessage = messageList.lastElementChild; + if (lastMessage) { + lastMessage.scrollIntoView({ block: 'end', inline: 'nearest' }); } +} + +function renderStreamContent(channel) { + const container = document.createElement('div'); + container.className = 'stream-container'; - emojiPickerListener = (event) => { - messageInput.value += event.detail.unicode; - picker.remove(); - }; + const streamInfo = document.createElement('div'); + streamInfo.className = 'stream-info'; - picker.addEventListener('emoji-click', emojiPickerListener); + const avatar = document.createElement('img'); + avatar.src = channel.avatarUrl || 'icons/default-avatar.png'; + avatar.alt = channel.displayName; + avatar.className = 'stream-avatar'; - // Position the picker above the input area - const inputContainer = document.querySelector('.message-input-container'); - inputContainer.appendChild(picker); + const details = document.createElement('div'); + details.className = 'stream-details'; - // Ensure picker is visible within viewport - const pickerRect = picker.getBoundingClientRect(); - const viewportHeight = window.innerHeight; + const link = document.createElement('a'); + link.href = 'https://radio.noderunners.org'; + link.target = '_blank'; + link.className = 'stream-link'; + link.textContent = 'Open Noderunners Radio'; - if (pickerRect.top < 0) { - // If picker would go above viewport, position it below the input instead - picker.style.bottom = 'auto'; - picker.style.top = '100%'; - picker.style.marginTop = '8px'; - picker.style.marginBottom = '0'; - } + details.innerHTML = ` +

${channel.displayName}

+

${channel.about}

`; - // Handle clicks outside the picker - const handleOutsideClick = (e) => { - if (!picker.contains(e.target) && e.target !== emojiButton) { - picker.remove(); - document.removeEventListener('click', handleOutsideClick); - } - }; + details.appendChild(link); - // Add slight delay to prevent immediate closure - setTimeout(() => { - document.addEventListener('click', handleOutsideClick); - }, 100); -}; - -emojiButton.addEventListener('click', emojiButtonListener); - -// Add GIF button functionality (placeholder) -gifButton.addEventListener('click', () => { - alert('GIF functionality coming soon!'); -}); + streamInfo.appendChild(avatar); + streamInfo.appendChild(details); + container.appendChild(streamInfo); + + return container; +} -async function handleStreamChat(pubkey, header, container) { +function handleStreamChat(pubkey, header, container) { const channel = contactManager.channels.get(pubkey); if (!channel) { container.innerHTML = '
Stream not available
'; @@ -634,18 +599,7 @@ async function handleStreamChat(pubkey, header, container) { } container.innerHTML = ''; - const videoContainer = document.createElement('div'); - videoContainer.className = 'video-container'; - - const iframe = document.createElement('iframe'); - iframe.src = `https://zap.stream`; - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.border = 'none'; - - videoContainer.appendChild(iframe); - container.appendChild(videoContainer); - + container.appendChild(renderStreamContent(channel)); updateChatHeader(header, channel); } @@ -672,7 +626,7 @@ async function subscribeToChannelEvents(channelPubkey) { function updateChatHeader(header, contact) { header.innerHTML = ` - ${contact.displayName} ${contact.displayName} @@ -717,78 +671,100 @@ async function sendMessage() { `; } else { bubbleElement.innerHTML = `
${linkifyText(content)}
`; + // Add link preview handling for new messages + setTimeout(() => loadLinkPreviews(), 100); } messageElement.appendChild(bubbleElement); messageList.appendChild(messageElement); messageElement.scrollIntoView({ behavior: 'smooth' }); - // Background send - await messageManager.sendMessage(currentChatPubkey, content); + // Ensure proper scrolling after sending + setTimeout(() => { + messageList.scrollTop = messageList.scrollHeight; + }, 100); + await messageManager.sendMessage(currentChatPubkey, content); } catch (error) { console.error('Failed to send message:', error); showErrorMessage('Failed to send message'); } } +async function renderMessage(message, metadata) { + // If metadata wasn't passed, try to fetch it + if (!metadata) { + metadata = await getUserMetadata(message.pubkey); + } + + const messageElement = document.createElement('div'); + messageElement.className = 'message'; + messageElement.setAttribute('data-message-id', message.id); + + const bubbleElement = document.createElement('div'); + bubbleElement.className = `message-bubble ${message.isSent ? 'sent' : 'received'}`; + + await renderMessageContent(message, bubbleElement); + messageElement.appendChild(bubbleElement); + + return messageElement; +} + async function renderMessageContent(message, bubbleElement) { const currentUser = await auth.getCurrentUser(); + const content = message.content; - // Handle decrypted message content - const content = message.decrypted; - + if (!content) { + console.error('No content in message:', message); + bubbleElement.innerHTML = '
Message could not be decrypted
'; + return; + } + // Check if content is a GIF URL from Giphy - if (content.includes('giphy.com')) { + if (typeof content === 'string' && content.includes('giphy.com')) { bubbleElement.innerHTML = `
`; + + // Add load event listener to update scroll position + const img = bubbleElement.querySelector('img'); + if (img) { + img.addEventListener('load', () => { + const messageList = document.querySelector('.message-list'); + if (messageList) { + messageList.scrollTop = messageList.scrollHeight; + } + }); + } return; } - - // Rest of your existing renderMessageContent logic - if (typeof content === 'object' && content.type) { - switch (content.type) { - case 'market-order': - bubbleElement.innerHTML = ` -
-
Order #${content.content.shipping_id}
-
- ${content.content.items.map(item => - `
- ${item.quantity}x - ${item.name} - ${item.price} -
` - ).join('')} -
- ${content.content.message ? - `
${content.content.message}
` : - ''} -
`; - break; - - case 'media': - const isVideo = content.mediaUrl?.match(/\.(mp4|webm|mov|ogg)$/i); - bubbleElement.innerHTML = ` -
- ${isVideo ? - `` : - `` - } - ${content.content ? - `
${linkifyText(content.content)}
` : - ''} -
`; - break; - default: - bubbleElement.innerHTML = `
${linkifyText(content.content)}
`; + // Plain text messages + bubbleElement.innerHTML = `
${linkifyText(content)}
`; + + // Add zap container if message is received + if (message.pubkey !== currentUser?.pubkey) { + const metadata = await getUserMetadata(message.pubkey); + if (metadata?.lud16 || metadata?.lightning) { + const zapContainer = document.createElement('div'); + zapContainer.className = 'zap-container'; + zapContainer.innerHTML = ` + + ${message.zapAmount || ''} + `; + + // Ensure container is positioned correctly + bubbleElement.style.position = 'relative'; + + const zapButton = zapContainer.querySelector('.zap-button'); + zapButton.addEventListener('click', async (e) => { + e.stopPropagation(); + await showZapModal(message, metadata, zapContainer); + }); + + bubbleElement.appendChild(zapContainer); } - } else { - // Plain text messages - bubbleElement.innerHTML = `
${linkifyText(content)}
`; } } @@ -796,11 +772,8 @@ function linkifyText(text) { return text .replace(/\n/g, '
') .replace(/(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|www\.[^\s<]+[^<.,:;"')\]\s]|[a-zA-Z0-9][a-zA-Z0-9-]+\.[a-zA-Z]{2,}\b(?:\/[^\s<]*)?)/g, (url) => { - if (url.match(/\.(jpg|jpeg|gif|png|mpwebm|mov|ogg)$/i)) { - return ''; // Don't show media URLs in text - } const fullUrl = url.startsWith('http') ? url : `https://${url}`; - return `${url.replace(/^https?:\/\//, '')}`; + return `${url}`; }); } @@ -906,33 +879,44 @@ function initializeEmojiPicker(emojiButton, messageInput) { // Replace the hardcoded streams array with a function to fetch stream data async function initializeStreamSection() { const streamData = { - pubkey: 'npub1ua6fxn9ktc4jncanf79jzvklgjftcdrt5etverwzzg0lgpmg3hsq2gh6v6', + pubkey: 'noderunnersradio', displayName: 'Noderunners Radio', isChannel: true, avatarUrl: 'https://image.nostr.build/9a9c9e5dba5ed17361f2f593dda02bd2ba85a14e69db1f251b27423f43864efe.webp', - streamUrl: 'naddr1qqjrvvt9vdjkzc3c943nxdmr956rqc3k95urjdps95crqdmrvd3nxvtxvdskxqghwaehxw309aex2mrp0yhxummnw3ezucnpdejz7qgewaehxw309aex2mrp0yh8xmn0wf6zuum0vd5kzmp0qy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcpz9mhxue69uhkummnw3ezumrpdejz7qg7waehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2ap0qyghwumn8ghj7mn0wd68ytnhd9hx2tcpz4mhxue69uhhyetvv9ujumn0wd68ytnzvuhsz9thwden5te0dehhxarj9ehhsarj9ejx2a30qgsv73dxhgfk8tt76gf6q788zrfyz9dwwgwfk3aar6l5gk82a76v9fgrqsqqqan8f2t2m0' + streamUrl: 'noderunnersradio' }; const channel = { ...streamData, - id: streamData.pubkey + id: streamData.pubkey, + embedUrl: `https://player.twitch.tv/?channel=${streamData.streamUrl}&parent=${location.hostname}&muted=true`, + about: '' }; - - contactManager.channels.set(channel.pubkey, channel); + + contactManager.channels.set(channel.id, channel); return channel; } document.getElementById('extensionLoginButton').addEventListener('click', async () => { try { - if (!window.nostr) { - showErrorMessage('No Nostr extension found.'); - return; - } - - const user = await auth.login('NIP-07'); - if (user) { + // Check if window.nostr exists after a short delay to allow extension injection + setTimeout(async () => { + if (typeof window.nostr === 'undefined') { + showErrorMessage('No Nostr extension found. Please install Alby or nos2x.'); + return; + } await handleSuccessfulLogin(user); - } + try { + await window.nostr.enable(); + const user = await auth.login('NIP-07'); + if (user) { + await handleSuccessfulLogin(user); + } + } catch (error) { + console.error('Extension login error:', error); + showErrorMessage('Extension login failed: ' + error.message); + } + }, 500); } catch (error) { console.error('Extension login error:', error); showErrorMessage('Extension login failed: ' + error.message); @@ -940,54 +924,494 @@ document.getElementById('extensionLoginButton').addEventListener('click', async }); async function loadLinkPreviews() { + // First handle regular link previews (keep existing code) const links = document.querySelectorAll('.message-text a'); - - for (const link of links) { + const mediaLinks = Array.from(links).filter(link => { + const content = link.href || link.textContent; + return content.includes('youtube.com') || + content.includes('youtu.be') || + content.includes('twitter.com') || + content.includes('x.com') || + content.includes('twitch.tv') || + content.includes('instagram.com') || + content.includes('tiktok.com') || + content.includes('iris.to') || + // Match both direct identifiers and nostr: protocol + content.match(/(?:nostr:)?(?:note|nevent|npub|nprofile|naddr)1[023456789acdefghjklmnpqrstuvwxyz]+/); + }); + + for (const link of mediaLinks) { try { const url = link.href; - if (link.nextElementSibling?.classList.contains('link-preview') || - url.match(/\.(jpg|jpeg|gif|png|mp4|webm|mov|ogg)$/i)) { - continue; - } + if (link.nextElementSibling?.classList.contains('link-preview')) continue; const previewContainer = document.createElement('div'); previewContainer.className = 'link-preview'; - const previewContent = document.createElement('div'); previewContent.className = 'link-preview-content'; - try { - const response = await fetch(url); - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const title = doc.querySelector('meta[property="og:title"]')?.content || - doc.querySelector('title')?.textContent || - new URL(url).hostname; - - const description = doc.querySelector('meta[property="og:description"]')?.content || - doc.querySelector('meta[name="description"]')?.content || ''; - - const image = doc.querySelector('meta[property="og:image"]')?.content; - - previewContent.innerHTML = ` - ${image ? `${title}` : ''} -
${title}
- ${description ? `
${description}
` : ''} - `; - } catch (err) { - console.warn('Failed to fetch preview:', err); - previewContent.innerHTML = ` -
${new URL(url).hostname}
- `; + if (url.includes('youtube.com') || url.includes('youtu.be')) { + const videoId = url.includes('youtube.com') ? + url.split('v=')[1]?.split('&')[0] : + url.split('youtu.be/')[1]?.split('?')[0]; + + if (videoId) { + previewContent.innerHTML = ` +
+ +
`; + } + } else if (url.includes('twitch.tv')) { + const channelName = url.split('twitch.tv/')[1]?.split('/')[0]; + if (channelName) { + previewContent.innerHTML = ` +
+ +
`; + } + } else if (url.includes('twitter.com') || url.includes('x.com')) { + const tweetId = url.split('/status/')[1]?.split('?')[0]; + if (tweetId) { + previewContent.innerHTML = ` + `; + } + } else if (url.includes('iris.to')) { + // Handle iris.to links + const npubMatch = url.match(/npub1[a-zA-Z0-9]+/); + if (npubMatch) { + try { + const pubkey = NostrTools.nip19.decode(npubMatch[0]).data; + const metadata = await getUserMetadata(pubkey); + previewContent.innerHTML = ` +
+
+ Avatar + ${metadata?.name || metadata?.displayName || shortenIdentifier(pubkey)} +
+ ${metadata?.about ? `
${metadata.about}
` : ''} +
`; + } catch (error) { + console.warn('Failed to decode iris.to npub:', error); + } + } + } else if (url.match(/(?:nostr:)?(?:note|nevent|npub|nprofile|naddr)1/) || url.match(/^(?:note|nevent|npub|nprofile|naddr)1/)) { + // Handle both direct identifiers and nostr: protocol + const nostrId = url.includes('nostr:') ? + url.split('nostr:')[1] : + (url.match(/(?:note|nevent|npub|nprofile|naddr)1[023456789acdefghjklmnpqrstuvwxyz]+/)?.[0] || url); + + try { + const decoded = NostrTools.nip19.decode(nostrId); + if (decoded.type === 'note' || decoded.type === 'nevent') { + const eventId = decoded.type === 'note' ? decoded.data : decoded.data.id; + const event = await pool.get(RELAYS, { ids: [eventId] }); + if (event) { + const metadata = await getUserMetadata(event.pubkey); + previewContent.innerHTML = ` +
+
+ Avatar + ${metadata?.name || metadata?.displayName || shortenIdentifier(event.pubkey)} +
+
${event.content}
+
`; + } + } else if (decoded.type === 'npub' || decoded.type === 'nprofile') { + const pubkey = decoded.type === 'npub' ? decoded.data : decoded.data.pubkey; + const metadata = await getUserMetadata(pubkey); + previewContent.innerHTML = ` +
+
+ Avatar + ${metadata?.name || metadata?.displayName || shortenIdentifier(pubkey)} +
+ ${metadata?.about ? `
${metadata.about}
` : ''} +
`; + } + } catch (error) { + console.warn('Failed to decode nostr link:', error); + } + + } + + if (previewContent.innerHTML) { + previewContainer.appendChild(previewContent); + link.parentNode.insertBefore(previewContainer, link.nextSibling); + } + } catch (error) { + console.warn('Failed to create media preview:', error); + } + } + + // Then handle raw nostr identifiers + const messageTexts = document.querySelectorAll('.message-text'); + for (const messageText of messageTexts) { + await handleRawNostrIdentifiers(messageText); + } +} + +async function showZapModal(message, metadata, zapContainer) { + const modal = document.createElement('div'); + modal.className = 'zap-modal'; + + // Get position of zap button + const zapButton = zapContainer.querySelector('.zap-button'); + const rect = zapButton.getBoundingClientRect(); + + modal.innerHTML = ` +
+

Send Zap

+ + +
+ `; + + document.body.appendChild(modal); + + // Position modal near the zap button + const modalContent = modal.querySelector('.zap-modal-content'); + modalContent.style.position = 'fixed'; + modalContent.style.top = `${rect.top}px`; + modalContent.style.left = `${rect.right + 10}px`; + + const sendButton = modal.querySelector('#sendZap'); + const amountInput = modal.querySelector('#zapAmount'); + + sendButton.addEventListener('click', async () => { + const amount = parseInt(amountInput.value); + if (amount > 0) { + await handleZap(message, metadata, amount, zapContainer); + modal.remove(); + } + }); + + // Close on click outside + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); + } + }); +} + +async function handleZap(message, metadata, amount, zapContainer) { + try { + const currentUser = await auth.getCurrentUser(); + const zapRequest = { + kind: 9734, + pubkey: currentUser.pubkey, + created_at: Math.floor(Date.now() / 1000), + content: "Zapped a DM", + tags: [ + ['p', message.pubkey], + ['e', message.id], + ['amount', amount.toString()], + ['relays', ...RELAYS] + ] + }; + + const invoice = await createZapInvoice(metadata, amount, zapRequest); + await showQRModal(invoice); + + } catch (error) { + console.error('Zap failed:', error); + showErrorMessage(error.message); + } +} + +async function createZapInvoice(metadata, amount, zapRequest) { + const lightningAddress = metadata.lud16 || metadata?.lightning; + if (!lightningAddress) throw new Error('No lightning address found'); + + try { + // Send request to background script to handle LNURL fetching + const response = await chrome.runtime.sendMessage({ + type: 'GET_ZAP_INVOICE', + data: { + lightningAddress, + amount, + zapRequest + } + }); + + if (response.error) { + throw new Error(response.error); + } + + return response.invoice; + } catch (error) { + console.error('Failed to create invoice:', error); + throw new Error('Could not generate Lightning invoice'); + } +} + +async function showQRModal(invoice) { + const modal = document.createElement('div'); + modal.className = 'qr-modal'; + + modal.innerHTML = ` +
+
+
+
${invoice}
+ +
+
+ `; + + document.body.appendChild(modal); + + // Use the global qrcode object + if (typeof window.qrcode === 'undefined') { + throw new Error('QR code library not loaded'); + } + + const qr = qrcode(0, "M"); + qr.addData(`lightning:${invoice}`); + qr.make(); + + const qrContainer = modal.querySelector('#qrcode-container'); + qrContainer.innerHTML = qr.createImgTag(4); + + const copyButton = modal.querySelector('.copy-button'); + const closeButton = modal.querySelector('.close-button'); + + copyButton.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(invoice); + copyButton.textContent = 'Copied!'; + copyButton.classList.add('copied'); + + setTimeout(() => { + copyButton.textContent = 'Copy Invoice'; + copyButton.classList.remove('copied'); + }, 2000); + } catch (error) { + console.error('Failed to copy invoice:', error); + showErrorMessage('Failed to copy invoice'); + } + }); + + closeButton.addEventListener('click', () => { + modal.remove(); + }); + + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); + } + }); +} + +chrome.runtime.onMessage.addListener((message) => { + if (message.type === 'ZAP_RECEIVED') { + const { messageId, amount } = message.data; + updateZapAmount(messageId, amount); + } + return true; // Important for Chrome message listeners +}); + +function updateZapAmount(messageId, amount) { + const messageElement = document.querySelector(`[data-message-id="${messageId}"]`); + if (messageElement) { + const zapAmount = messageElement.querySelector('.zap-amount'); + const zapButton = messageElement.querySelector('.zap-button'); + + if (zapAmount && zapButton) { + zapAmount.textContent = amount; + zapButton.classList.add('zap-received'); + setTimeout(() => { + zapButton.classList.remove('zap-received'); + }, 500); + } + } +} + +function hasLightningAddress(metadata) { + return !!(metadata?.lud16 || metadata?.lightning); +} + +// Function to load search history +async function loadSearchHistory() { + try { + const result = await chrome.storage.local.get('searchHistory'); + searchHistory = result.searchHistory || []; + } catch (error) { + console.error('Failed to load search history:', error); + searchHistory = []; + } +} + +// Function to save search history +async function saveSearchHistory(term) { + try { + // First get the current history + const result = await chrome.storage.local.get('searchHistory'); + let currentHistory = result.searchHistory || []; + + // Only add if term isn't already in history + if (!currentHistory.includes(term)) { + currentHistory.unshift(term); + if (currentHistory.length > 5) currentHistory.pop(); + + // Save the updated history + await chrome.storage.local.set({ searchHistory: currentHistory }); + // Update the global searchHistory variable + searchHistory = currentHistory; + } + } catch (error) { + console.error('Failed to save search history:', error); + } +} + +// Modify the search input event listeners +function initializeSearchInput() { + const searchInput = document.getElementById('searchInput'); + const clearSearchButton = document.getElementById('clearSearch'); + const datalist = document.getElementById('search-history'); + + // Populate datalist with existing history + datalist.innerHTML = searchHistory + .map(term => `