diff --git a/package.json b/package.json index 7041e5d..afcca3c 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,11 @@ "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", + "@resreq/event-hub": "^1.6.0", "@resreq/timer": "^1.1.5", + "@rtco/client": "^0.2.17", "@tailwindcss/typography": "^0.5.15", + "@webext-core/proxy-service": "^1.2.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64d5244..a937f0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,12 +44,21 @@ importers: '@radix-ui/react-switch': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@resreq/event-hub': + specifier: ^1.6.0 + version: 1.6.0 '@resreq/timer': specifier: ^1.1.5 version: 1.1.5 + '@rtco/client': + specifier: ^0.2.17 + version: 0.2.17 '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.15(tailwindcss@3.4.12) + '@webext-core/proxy-service': + specifier: ^1.2.0 + version: 1.2.0(@webext-core/messaging@1.4.0)(webextension-polyfill@0.12.0) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -115,7 +124,7 @@ importers: version: 2.5.2 trystero: specifier: ^0.20.0 - version: 0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20) + version: 0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4) type-fest: specifier: ^4.26.1 version: 4.26.1 @@ -227,7 +236,7 @@ importers: version: 6.0.1 wxt: specifier: ^0.19.9 - version: 0.19.9(@types/node@22.5.5)(rollup@4.21.3) + version: 0.19.9(@types/node@22.5.5)(bufferutil@4.0.8)(rollup@4.21.3)(utf-8-validate@6.0.4) packages: @@ -1741,6 +1750,12 @@ packages: cpu: [x64] os: [win32] + '@rtco/client@0.2.17': + resolution: {integrity: sha512-nV/KJGBh/j0fK069uADXNr30JBbWe7CZ0xe0cEx1JjuBQIkH10Ny5mQm4WZd/vID+EF7l+tqkCG6QUyCAW5PFg==} + + '@rtco/peer@0.2.17': + resolution: {integrity: sha512-jxKQzAIMiofkJ5UHIbeq2JUl+fBOCnWRxgxemzuI7TKw96pbkfaMowr3fj+ElXnKPWwCi5WRX3hwitqjfNkwFQ==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1756,6 +1771,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@supabase/auth-js@2.65.0': resolution: {integrity: sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==} @@ -2105,6 +2123,15 @@ packages: '@webext-core/match-patterns@1.0.3': resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==} + '@webext-core/messaging@1.4.0': + resolution: {integrity: sha512-gzXQ13HfKR3Yrn9TnrvTC/5seA7uPFvaqxqNFBsFOOdSZa5LyXt58Rhym8BYXarkWUGp+fh8f6AYM3RYuNbS+A==} + + '@webext-core/proxy-service@1.2.0': + resolution: {integrity: sha512-MCUadVakeb7L47AvdtlbJfBUDjFdejr5t4E2WrwZagnev3a5I/xh2wHCkE+G0ihO/VUt/m0R1MPX+y4YVFRyPA==} + peerDependencies: + '@webext-core/messaging': '>=1.3.1' + webextension-polyfill: ^0.10.0 + '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} @@ -2311,6 +2338,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bufferutil@4.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} + engines: {node: '>=6.14.2'} + bundle-name@3.0.0: resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} engines: {node: '>=12'} @@ -2825,6 +2856,13 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-client@6.6.1: + resolution: {integrity: sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -3271,6 +3309,10 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} + get-value@3.0.1: + resolution: {integrity: sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==} + engines: {node: '>=6.0'} + giget@1.2.3: resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} hasBin: true @@ -4415,6 +4457,10 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-gyp-build@4.8.2: + resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} + hasBin: true + node-notifier@10.0.1: resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} @@ -5128,6 +5174,10 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-error@11.0.3: + resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==} + engines: {node: '>=14.16'} + serialize-error@9.1.1: resolution: {integrity: sha512-6uZQLGyUkNA4N+Zii9fYukmNu9PEA1F5rqcwXzN/3LtBjwl2dFBbVZ1Zyn08/CGkB4H440PIemdOQBt1Wvjbrg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5211,6 +5261,14 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + socket.io-client@4.8.0: + resolution: {integrity: sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + sonner@1.5.0: resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==} peerDependencies: @@ -5756,6 +5814,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + utf-8-validate@6.0.4: + resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==} + engines: {node: '>=6.14.2'} + utf8-bytes@0.0.1: resolution: {integrity: sha512-GifWmJAx2qAXT+lZLhbkWhBsy7pr6xWHiPWlVToDiELdWgZwt4Ogjf9tlgvKuALzTFR/d+EPQQI9ogJV3957Jg==} @@ -5849,6 +5911,9 @@ packages: webext-bridge@6.0.1: resolution: {integrity: sha512-GruIrN+vNwbxVCi8UW4Dqk5YkcGA9V0ZfJ57jXP9JXHbrsDs5k2N6NNYQR5e+wSCnQpGYOGAGihwUpKlhg8QIw==} + webextension-polyfill@0.10.0: + resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} + webextension-polyfill@0.12.0: resolution: {integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==} @@ -5941,6 +6006,18 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -5969,6 +6046,10 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xmlhttprequest-ssl@2.1.1: + resolution: {integrity: sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==} + engines: {node: '>=0.4.0'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -7186,7 +7267,7 @@ snapshots: uint8arraylist: 2.4.8 uint8arrays: 5.1.0 - '@libp2p/websockets@8.2.0': + '@libp2p/websockets@8.2.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@libp2p/interface': 1.7.0 '@libp2p/utils': 5.4.9 @@ -7194,12 +7275,12 @@ snapshots: '@multiformats/multiaddr': 12.3.1 '@multiformats/multiaddr-to-uri': 10.1.0 '@types/ws': 8.5.12 - it-ws: 6.1.5 + it-ws: 6.1.5(bufferutil@4.0.8)(utf-8-validate@6.0.4) p-defer: 4.0.1 progress-events: 1.0.1 race-signal: 1.1.0 wherearewe: 2.0.1 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7781,6 +7862,21 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.21.3': optional: true + '@rtco/client@0.2.17': + dependencies: + '@rtco/peer': 0.2.17 + bufferutil: 4.0.8 + eventemitter3: 5.0.1 + nanoid: 5.0.7 + socket.io-client: 4.8.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + utf-8-validate: 6.0.4 + transitivePeerDependencies: + - supports-color + + '@rtco/peer@0.2.17': + dependencies: + eventemitter3: 5.0.1 + '@sec-ant/readable-stream@0.4.1': {} '@sindresorhus/fnv1a@3.1.0': {} @@ -7789,6 +7885,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@socket.io/component-emitter@3.1.2': {} + '@supabase/auth-js@2.65.0': dependencies: '@supabase/node-fetch': 2.6.15 @@ -7805,12 +7903,12 @@ snapshots: dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/realtime-js@2.10.2': + '@supabase/realtime-js@2.10.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@supabase/node-fetch': 2.6.15 '@types/phoenix': 1.6.5 '@types/ws': 8.5.12 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -7819,13 +7917,13 @@ snapshots: dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/supabase-js@2.45.4': + '@supabase/supabase-js@2.45.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@supabase/auth-js': 2.65.0 '@supabase/functions-js': 2.4.1 '@supabase/node-fetch': 2.6.15 '@supabase/postgrest-js': 1.16.1 - '@supabase/realtime-js': 2.10.2 + '@supabase/realtime-js': 2.10.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@supabase/storage-js': 2.7.0 transitivePeerDependencies: - bufferutil @@ -8213,13 +8311,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@waku/sdk@0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)': + '@waku/sdk@0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@chainsafe/libp2p-noise': 14.1.0 '@libp2p/identify': 1.0.21 '@libp2p/mplex': 10.1.5 '@libp2p/ping': 1.1.6 - '@libp2p/websockets': 8.2.0 + '@libp2p/websockets': 8.2.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@noble/hashes': 1.5.0 '@waku/core': 0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4) '@waku/discovery': 0.0.3(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20) @@ -8267,6 +8365,17 @@ snapshots: '@webext-core/match-patterns@1.0.3': {} + '@webext-core/messaging@1.4.0': + dependencies: + serialize-error: 11.0.3 + webextension-polyfill: 0.10.0 + + '@webext-core/proxy-service@1.2.0(@webext-core/messaging@1.4.0)(webextension-polyfill@0.12.0)': + dependencies: + '@webext-core/messaging': 1.4.0 + get-value: 3.0.1 + webextension-polyfill: 0.12.0 + '@xobotyi/scrollbar-width@1.9.5': {} JSONStream@1.3.5: @@ -8482,6 +8591,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bufferutil@4.0.8: + dependencies: + node-gyp-build: 4.8.2 + bundle-name@3.0.0: dependencies: run-applescript: 5.0.0 @@ -9011,6 +9124,20 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) + xmlhttprequest-ssl: 2.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + entities@4.5.0: {} env-paths@2.2.1: {} @@ -9669,6 +9796,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 + get-value@3.0.1: + dependencies: + isobject: 3.0.1 + giget@1.2.3: dependencies: citty: 0.1.6 @@ -10209,13 +10340,13 @@ snapshots: it-take@3.0.6: {} - it-ws@6.1.5: + it-ws@6.1.5(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: '@types/ws': 8.5.12 event-iterator: 2.0.0 it-stream-types: 2.0.2 uint8arrays: 5.1.0 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -10932,7 +11063,7 @@ snapshots: transitivePeerDependencies: - supports-color - mqtt@5.10.1: + mqtt@5.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: '@types/readable-stream': 4.0.15 '@types/ws': 8.5.12 @@ -10949,7 +11080,7 @@ snapshots: rfdc: 1.4.1 split2: 4.2.0 worker-timers: 7.1.8 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - supports-color @@ -11044,6 +11175,8 @@ snapshots: node-forge@1.3.1: {} + node-gyp-build@4.8.2: {} + node-notifier@10.0.1: dependencies: growly: 1.3.0 @@ -11863,6 +11996,10 @@ snapshots: semver@7.6.3: {} + serialize-error@11.0.3: + dependencies: + type-fest: 2.19.0 + serialize-error@9.1.1: dependencies: type-fest: 2.19.0 @@ -11945,6 +12082,24 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + socket.io-client@4.8.0(bufferutil@4.0.8)(utf-8-validate@6.0.4): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + sonner@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -12246,16 +12401,16 @@ snapshots: trough@2.2.0: {} - trystero@0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20): + trystero@0.20.0(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/proto@0.0.7)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: '@noble/curves': 1.6.0 - '@supabase/supabase-js': 2.45.4 + '@supabase/supabase-js': 2.45.4(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@thaunknown/simple-peer': 10.0.10 '@waku/discovery': 0.0.3(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20) - '@waku/sdk': 0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20) + '@waku/sdk': 0.0.26(@libp2p/interface@1.7.0)(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/enr@0.0.21)(@waku/interfaces@0.0.22)(@waku/message-hash@0.1.16)(@waku/relay@0.0.11(@waku/core@0.0.27(@multiformats/multiaddr@12.3.1)(libp2p@1.9.4))(@waku/interfaces@0.0.22)(@waku/proto@0.0.7)(@waku/utils@0.0.20))(@waku/utils@0.0.20)(bufferutil@4.0.8)(utf-8-validate@6.0.4) firebase: 10.13.1 libp2p: 1.9.4 - mqtt: 5.10.1 + mqtt: 5.10.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - '@libp2p/bootstrap' - '@libp2p/interface' @@ -12541,6 +12696,10 @@ snapshots: dependencies: react: 18.3.1 + utf-8-validate@6.0.4: + dependencies: + node-gyp-build: 4.8.2 + utf8-bytes@0.0.1: {} utf8-codec@1.0.0: {} @@ -12614,7 +12773,7 @@ snapshots: ms: 3.0.0-canary.1 supports-color: 9.4.0 - web-ext-run@0.2.1: + web-ext-run@0.2.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: '@babel/runtime': 7.24.7 '@devicefarmer/adbkit': 3.2.6 @@ -12638,7 +12797,7 @@ snapshots: tmp: 0.2.3 update-notifier: 6.0.2 watchpack: 2.4.1 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) zip-dir: 2.0.0 transitivePeerDependencies: - bufferutil @@ -12653,6 +12812,8 @@ snapshots: tiny-uid: 1.1.2 webextension-polyfill: 0.9.0 + webextension-polyfill@0.10.0: {} + webextension-polyfill@0.12.0: {} webextension-polyfill@0.9.0: {} @@ -12768,9 +12929,17 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - ws@8.18.0: {} + ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 6.0.4 - wxt@0.19.9(@types/node@22.5.5)(rollup@4.21.3): + ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 6.0.4 + + wxt@0.19.9(@types/node@22.5.5)(bufferutil@4.0.8)(rollup@4.21.3)(utf-8-validate@6.0.4): dependencies: '@aklinker1/rollup-plugin-visualizer': 5.12.0(rollup@4.21.3) '@types/chrome': 0.0.269 @@ -12814,7 +12983,7 @@ snapshots: unimport: 3.12.0(rollup@4.21.3) vite: 5.4.5(@types/node@22.5.5) vite-node: 2.1.1(@types/node@22.5.5) - web-ext-run: 0.2.1 + web-ext-run: 0.2.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) webextension-polyfill: 0.12.0 transitivePeerDependencies: - '@types/node' @@ -12840,6 +13009,8 @@ snapshots: xmlbuilder@11.0.1: {} + xmlhttprequest-ssl@2.1.1: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/src/app/background/index.ts b/src/app/background/index.ts index 2c35cdc..6248cb0 100644 --- a/src/app/background/index.ts +++ b/src/app/background/index.ts @@ -1,3 +1,4 @@ +import { EVENT } from '@/constants/event' import { browser } from 'wxt/browser' import { defineBackground } from 'wxt/sandbox' @@ -7,8 +8,10 @@ export default defineBackground({ type: 'module', main() { - browser.runtime.onMessage.addListener(async () => { - browser.runtime.openOptionsPage() + browser.runtime.onMessage.addListener(async (event: EVENT) => { + if (event === EVENT.OPEN_OPTIONS_PAGE) { + browser.runtime.openOptionsPage() + } }) } }) diff --git a/src/app/content/App.tsx b/src/app/content/App.tsx index ab271c2..3f49525 100644 --- a/src/app/content/App.tsx +++ b/src/app/content/App.tsx @@ -5,15 +5,12 @@ import AppButton from '@/app/content/views/AppButton' import AppContainer from '@/app/content/views/AppContainer' import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react' import RoomDomain from '@/domain/Room' -import { stringToHex } from '@/utils' import { Toaster } from '@/components/ui/Sonner' import UserInfoDomain from '@/domain/UserInfo' import Setup from '@/app/content/views/Setup' import MessageListDomain from '@/domain/MessageList' import { useEffect } from 'react' -const hostRoomId = stringToHex(document.location.host) - export default function App() { const send = useRemeshSend() const roomDomain = useRemeshDomain(RoomDomain()) @@ -29,7 +26,7 @@ export default function App() { useEffect(() => { if (userInfoFinished) { if (userInfo) { - !roomFinished && send(roomDomain.command.JoinRoomCommand(hostRoomId)) + !roomFinished && send(roomDomain.command.JoinRoomCommand()) } else { send(messageListDomain.command.ClearListCommand()) } diff --git a/src/app/content/components/PromptItem.tsx b/src/app/content/components/PromptItem.tsx index d17dae0..aaa1022 100644 --- a/src/app/content/components/PromptItem.tsx +++ b/src/app/content/components/PromptItem.tsx @@ -12,7 +12,7 @@ export interface PromptItemProps { const PromptItem: FC = ({ data, className }) => { return ( -
+
diff --git a/src/app/content/index.tsx b/src/app/content/index.tsx index a46603f..3af444e 100644 --- a/src/app/content/index.tsx +++ b/src/app/content/index.tsx @@ -8,7 +8,8 @@ import { createShadowRootUi } from 'wxt/client' import App from './App' import { IndexDBStorageImpl, BrowserSyncStorageImpl } from '@/domain/impls/Storage' -import { PeerRoomImpl } from '@/domain/impls/PeerRoom' +// import { PeerRoomImpl } from '@/domain/impls/PeerRoom' +import { PeerRoomImpl } from '@/domain/impls/PeerRoom2' import '@/assets/styles/tailwind.css' import { createElement } from '@/utils' import { ToastImpl } from '@/domain/impls/Toast' @@ -19,7 +20,7 @@ export default defineContentScript({ async main(ctx) { const store = Remesh.store({ externs: [IndexDBStorageImpl, BrowserSyncStorageImpl, PeerRoomImpl, ToastImpl], - inspectors: !__DEV__ ? [RemeshLogger()] : [] + inspectors: __DEV__ ? [RemeshLogger()] : [] }) const ui = await createShadowRootUi(ctx, { diff --git a/src/app/content/views/Header/index.tsx b/src/app/content/views/Header/index.tsx index a4a7382..1f225c1 100644 --- a/src/app/content/views/Header/index.tsx +++ b/src/app/content/views/Header/index.tsx @@ -6,13 +6,12 @@ import { Button } from '@/components/ui/Button' import { getSiteInfo } from '@/utils' import { useRemeshDomain, useRemeshQuery } from 'remesh-react' import RoomDomain from '@/domain/Room' -import { selfId } from 'trystero' const Header: FC = () => { const siteInfo = getSiteInfo() const roomDomain = useRemeshDomain(RoomDomain()) const userList = useRemeshQuery(roomDomain.query.UserListQuery()) - console.log('userList', [...userList], userList.length) + const peerId = useRemeshQuery(roomDomain.query.PeerIdQuery()) return (
@@ -27,7 +26,7 @@ const Header: FC = () => { diff --git a/src/app/options/components/AvatarSelect.tsx b/src/app/options/components/AvatarSelect.tsx index 511e64b..acb599d 100644 --- a/src/app/options/components/AvatarSelect.tsx +++ b/src/app/options/components/AvatarSelect.tsx @@ -21,8 +21,8 @@ const AvatarSelect = React.forwardRef( const handleChange = async (e: ChangeEvent) => { const file = e.target.files?.[0] if (file) { - if (!/image\/(png|jpeg)/.test(file.type)) { - onWarning?.(new Error('Only PNG and JPEG image are supported.')) + if (!/image\/(png|jpeg|webp)/.test(file.type)) { + onWarning?.(new Error('Only PNG, JPEG and WebP image are supported.')) return } diff --git a/src/app/options/components/ProfileForm.tsx b/src/app/options/components/ProfileForm.tsx index 626174c..fa4ddd5 100644 --- a/src/app/options/components/ProfileForm.tsx +++ b/src/app/options/components/ProfileForm.tsx @@ -42,7 +42,7 @@ const formSchema = v.object({ avatar: v.pipe( v.string(), v.notLength(0, 'Please select your avatar.'), - v.maxBytes(8 * 1024, 'Your avatar cannot exceed 8kb.') + v.maxBytes(8 * 1024, `Your avatar cannot exceed 8kb.`) ), themeMode: v.pipe( v.string(), @@ -92,32 +92,24 @@ const ProfileForm = () => { control={form.control} name="avatar" render={({ field }) => ( - + -
- - -
+
)} /> - + ({ + name: 'Room.PeerIdState', + default: peerRoom.peerId + }) + + const PeerIdQuery = domain.query({ + name: 'Room.PeerIdQuery', + impl: ({ get }) => { + return get(PeerIdState()) + } + }) + const MessageListQuery = messageListDomain.query.ListQuery const RoomStatusModule = StatusModule(domain, { @@ -71,16 +83,17 @@ const RoomDomain = Remesh.domain({ const JoinRoomCommand = domain.command({ name: 'RoomJoinRoomCommand', - impl: ({ get }, roomId: string) => { - peerRoom.joinRoom(roomId) + impl: ({ get }) => { + peerRoom.joinRoom() const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! + return [ - JoinRoomEvent(roomId), - RoomStatusModule.command.SetFinishedCommand(), UpdateUserListCommand({ type: 'create', - user: { peerId: peerRoom.selfId, joinTime: Date.now(), userId, username, userAvatar } - }) + user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar } + }), + RoomStatusModule.command.SetFinishedCommand(), + JoinRoomEvent(peerRoom.roomId) ] } }) @@ -91,12 +104,12 @@ const RoomDomain = Remesh.domain({ peerRoom.leaveRoom() const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! return [ - LeaveRoomEvent(roomId), - RoomStatusModule.command.SetInitialCommand(), UpdateUserListCommand({ type: 'delete', - user: { peerId: peerRoom.selfId, joinTime: Date.now(), userId, username, userAvatar } - }) + user: { peerId: peerRoom.peerId, joinTime: Date.now(), userId, username, userAvatar } + }), + RoomStatusModule.command.SetInitialCommand(), + LeaveRoomEvent(roomId) ] } }) @@ -105,22 +118,26 @@ const RoomDomain = Remesh.domain({ name: 'RoomSendTextMessageCommand', impl: ({ get }, message: string) => { const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - const id = nanoid() - const date = Date.now() - return [ - messageListDomain.command.CreateItemCommand({ - id, - type: MessageType.Normal, - body: message, - date, - userId, - username, - userAvatar, - likeUsers: [], - hateUsers: [] - }), - SendTextMessageEvent({ id, body: message, userId, username, userAvatar, type: SendType.Text }) - ] + + const textMessage: TextMessage = { + id: nanoid(), + type: SendType.Text, + body: message, + userId, + username, + userAvatar + } + + const listMessage: NormalMessage = { + ...textMessage, + type: MessageType.Normal, + date: Date.now(), + likeUsers: [], + hateUsers: [] + } + + peerRoom.sendMessage(textMessage) + return [messageListDomain.command.CreateItemCommand(listMessage), SendTextMessageEvent(textMessage)] } }) @@ -128,28 +145,23 @@ const RoomDomain = Remesh.domain({ name: 'RoomSendLikeMessageCommand', impl: ({ get }, messageId: string) => { const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - const _message = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage - return [ - messageListDomain.command.UpdateItemCommand({ - ..._message, - likeUsers: desert( - _message.likeUsers, - { - userId, - username, - userAvatar - }, - 'userId' - ) - }), - SendLikeMessageEvent({ - id: messageId, - userId, - username, - userAvatar, - type: SendType.Like - }) - ] + const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage + + const likeMessage: LikeMessage = { + id: messageId, + userId, + username, + userAvatar, + type: SendType.Like + } + const listMessage: NormalMessage = { + ...localMessage, + type: MessageType.Normal, + date: Date.now(), + likeUsers: desert(localMessage.likeUsers, likeMessage, 'userId') + } + peerRoom.sendMessage(likeMessage) + return [messageListDomain.command.UpdateItemCommand(listMessage), SendLikeMessageEvent(likeMessage)] } }) @@ -157,47 +169,55 @@ const RoomDomain = Remesh.domain({ name: 'RoomSendHateMessageCommand', impl: ({ get }, messageId: string) => { const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - const _message = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage + const localMessage = get(messageListDomain.query.ItemQuery(messageId)) as NormalMessage - return [ - messageListDomain.command.UpdateItemCommand({ - ..._message, - hateUsers: desert( - _message.hateUsers, - { - userId, - username, - userAvatar - }, - 'userId' - ) - }), - SendHateMessageEvent({ id: messageId, userId, username, userAvatar, type: SendType.Hate }) - ] + const hateMessage: HateMessage = { + id: messageId, + userId, + username, + userAvatar, + type: SendType.Hate + } + const listMessage: NormalMessage = { + ...localMessage, + type: MessageType.Normal, + date: Date.now(), + hateUsers: desert(localMessage.hateUsers, hateMessage, 'userId') + } + peerRoom.sendMessage(hateMessage) + return [messageListDomain.command.UpdateItemCommand(listMessage), SendHateMessageEvent(hateMessage)] } }) const SendUserSyncMessageCommand = domain.command({ name: 'RoomSendUserSyncMessageCommand', impl: ({ get }, targetPeerId: string) => { - const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - const joinTime = get(UserListQuery()).find((u) => u.peerId === peerRoom.selfId)?.joinTime || Date.now() - return [ - SendUserSyncMessageEvent({ - id: nanoid(), - peerId: peerRoom.selfId, - targetPeerId, - userId, - joinTime, - username, - userAvatar, - type: SendType.UserSync - }) - ] + const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)! + + const syncUserMessage: SyncUserMessage = { + ...self, + id: nanoid(), + type: SendType.UserSync + } + + peerRoom.sendMessage(syncUserMessage, targetPeerId) + return [SendUserSyncMessageEvent(syncUserMessage)] } }) - const SendUserSyncMessageEvent = domain.event({ + const UpdateUserListCommand = domain.command({ + name: 'RoomUpdateUserListCommand', + impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => { + const userList = get(UserListState()) + if (action.type === 'create') { + return [UserListState().new(upsert(userList, action.user, 'peerId'))] + } else { + return [UserListState().new(userList.filter(({ peerId }) => peerId !== action.user.peerId))] + } + } + }) + + const SendUserSyncMessageEvent = domain.event({ name: 'RoomSendUserSyncMessageEvent' }) @@ -233,94 +253,47 @@ const RoomDomain = Remesh.domain({ name: 'RoomOnLeaveRoomEvent' }) - const UpdateUserListCommand = domain.command({ - name: 'RoomUpdateUserListCommand', - impl: ({ get }, action: { type: 'create' | 'delete'; user: RoomUser }) => { - const userList = get(UserListState()) - if (action.type === 'create') { - return [UserListState().new(upsert(userList, action.user, 'peerId'))] - } else { - return [UserListState().new(userList.filter(({ peerId }) => peerId !== action.user.peerId))] - } - } - }) - domain.effect({ - name: 'RoomSendTextMessageEffect', - impl: ({ fromEvent }) => { - const sendMessage$ = fromEvent(SendTextMessageEvent).pipe( - tap(async (message) => { - peerRoom.sendMessage(message) + name: 'RoomOnJoinRoomEffect', + impl: () => { + const onJoinRoom$ = callbackToObservable(peerRoom.onJoinRoom).pipe( + mergeMap((peerId) => { + console.log('onJoinRoom', peerId) + if (peerRoom.peerId === peerId) { + return [OnJoinRoomEvent(peerId)] + } else { + return [SendUserSyncMessageCommand(peerId), OnJoinRoomEvent(peerId)] + } }) ) - return merge(sendMessage$).pipe(map(() => null)) - } - }) - - domain.effect({ - name: 'RoomSendLikeMessageEffect', - impl: ({ fromEvent }) => { - const likeMessage$ = fromEvent(SendLikeMessageEvent).pipe( - tap(async (message) => { - return peerRoom.sendMessage(message) - }) - ) - return merge(likeMessage$).pipe(map(() => null)) - } - }) - - domain.effect({ - name: 'RoomSendHateMessageEffect', - impl: ({ fromEvent }) => { - const hateMessage$ = fromEvent(SendHateMessageEvent).pipe( - tap(async (message) => { - peerRoom.sendMessage(message) - }) - ) - return merge(hateMessage$).pipe(map(() => null)) - } - }) - - domain.effect({ - name: 'RoomSendUserSyncMessageEffect', - impl: ({ fromEvent }) => { - const userSyncMessage$ = fromEvent(SendUserSyncMessageEvent).pipe( - tap(async (message) => { - console.log('sendMessage', message) - - peerRoom.sendMessage(message, message.targetPeerId) - }) - ) - return merge(userSyncMessage$).pipe(map(() => null)) + return onJoinRoom$ } }) domain.effect({ name: 'RoomOnMessageEffect', - impl: ({ fromEvent, get }) => { - const onMessage$ = fromEvent(JoinRoomEvent).pipe( - switchMap(() => callbackToObservable(peerRoom.onMessage.bind(peerRoom))), + impl: ({ get }) => { + const onMessage$ = callbackToObservable(peerRoom.onMessage).pipe( mergeMap((message) => { console.log('onMessage', message) - const messageEvent$ = of(OnMessageEvent(message)) const commandEvent$ = (() => { switch (message.type) { case SendType.UserSync: { - const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.selfId)! - if (self.joinTime > message.joinTime) { - return EMPTY - } + const self = get(UserListQuery()).find((user) => user.peerId === peerRoom.peerId)! + const isJoining = self.joinTime < message.joinTime return of( UpdateUserListCommand({ type: 'create', user: message }), - messageListDomain.command.CreateItemCommand({ - ...message, - id: nanoid(), - body: `"${message.username}" joined the chat`, - type: MessageType.Prompt, - date: Date.now() - }) + isJoining + ? messageListDomain.command.CreateItemCommand({ + ...message, + id: nanoid(), + body: `"${message.username}" joined the chat`, + type: MessageType.Prompt, + date: Date.now() + }) + : null ) } case SendType.Text: @@ -356,7 +329,7 @@ const RoomDomain = Remesh.domain({ ) } default: - console.warn('未知消息类型', message) + console.warn('Unsupported message type', message) return EMPTY } })() @@ -367,36 +340,14 @@ const RoomDomain = Remesh.domain({ } }) - domain.effect({ - name: 'RoomOnJoinRoomEffect', - impl: ({ fromEvent, get }) => { - const onJoinRoom$ = fromEvent(JoinRoomEvent).pipe( - switchMap(() => callbackToObservable(peerRoom.onJoinRoom.bind(peerRoom))), - mergeMap((peerId) => { - console.log('onJoinRoom', peerId) - const { id: userId, name: username, avatar: userAvatar } = get(userInfoDomain.query.UserInfoQuery())! - return [ - SendUserSyncMessageCommand(peerId), - UpdateUserListCommand({ - type: 'create', - user: { peerId, joinTime: Date.now(), userId, username, userAvatar } - }), - OnJoinRoomEvent(peerId) - ] - }) - ) - return onJoinRoom$ - } - }) - domain.effect({ name: 'RoomOnLeaveRoomEffect', - impl: ({ fromEvent, get }) => { - const onLeaveRoom$ = fromEvent(JoinRoomEvent).pipe( - switchMap(() => callbackToObservable(peerRoom.onLeaveRoom.bind(peerRoom))), + impl: ({ get }) => { + const onLeaveRoom$ = callbackToObservable(peerRoom.onLeaveRoom).pipe( map((peerId) => { console.log('onLeaveRoom', peerId) const user = get(UserListQuery()).find((user) => user.peerId === peerId) + if (user) { return [ UpdateUserListCommand({ type: 'delete', user }), @@ -420,6 +371,7 @@ const RoomDomain = Remesh.domain({ return { query: { + PeerIdQuery, UserListQuery, MessageListQuery, ...RoomStatusModule.query diff --git a/src/domain/externs/PeerRoom.ts b/src/domain/externs/PeerRoom.ts index 8b28e70..ff388e5 100644 --- a/src/domain/externs/PeerRoom.ts +++ b/src/domain/externs/PeerRoom.ts @@ -1,25 +1,27 @@ import { Remesh } from 'remesh' -import { type Promisable } from 'type-fest' export type PeerMessage = object | Blob | ArrayBuffer | ArrayBufferView export interface PeerRoom { - readonly selfId: string - joinRoom: (roomId: string) => Promise - sendMessage: (message: T, id?: string) => Promise - onMessage: (callback: (message: T) => void) => Promisable - leaveRoom: () => Promisable - onJoinRoom: (callback: (id: string) => void) => Promisable - onLeaveRoom: (callback: (id: string) => void) => Promisable + readonly peerId: string + readonly roomId: string + joinRoom: () => PeerRoom + sendMessage: (message: T, id?: string) => PeerRoom + onMessage: (callback: (message: T) => void) => PeerRoom + leaveRoom: () => PeerRoom + onJoinRoom: (callback: (id: string) => void) => PeerRoom + onLeaveRoom: (callback: (id: string) => void) => PeerRoom + onError: (callback: (error: Error) => void) => PeerRoom } export const PeerRoomExtern = Remesh.extern({ default: { - selfId: '', - joinRoom: async () => { + peerId: '', + roomId: '', + joinRoom: () => { throw new Error('"joinRoom" not implemented.') }, - sendMessage: async () => { + sendMessage: () => { throw new Error('"sendMessage" not implemented.') }, onMessage: () => { @@ -33,6 +35,9 @@ export const PeerRoomExtern = Remesh.extern({ }, onLeaveRoom: () => { throw new Error('"onLeaveRoom" not implemented.') + }, + onError: () => { + throw new Error('"onError" not implemented.') } } }) diff --git a/src/domain/impls/PeerRoom.ts b/src/domain/impls/PeerRoom.ts index 972bd9e..307357d 100644 --- a/src/domain/impls/PeerRoom.ts +++ b/src/domain/impls/PeerRoom.ts @@ -1,64 +1,151 @@ import { type DataPayload, type Room, joinRoom, selfId } from 'trystero' // import { joinRoom } from 'trystero/firebase' + import { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom' import { stringToHex } from '@/utils' +import EventHub from '@resreq/event-hub' export interface Config { - appId: string + peerId?: string + roomId: string } -class PeerRoom { +class PeerRoom extends EventHub { readonly appId: string - room: Room | null - readonly selfId: string + private room?: Room + readonly roomId: string + readonly peerId: string constructor(config: Config) { - this.appId = config.appId - this.room = null - this.selfId = selfId + super() + this.appId = __NAME__ + this.roomId = config.roomId + this.peerId = selfId + this.joinRoom = this.joinRoom.bind(this) + this.sendMessage = this.sendMessage.bind(this) + this.onMessage = this.onMessage.bind(this) + this.onJoinRoom = this.onJoinRoom.bind(this) + this.onLeaveRoom = this.onLeaveRoom.bind(this) + this.leaveRoom = this.leaveRoom.bind(this) + this.onError = this.onError.bind(this) } - async joinRoom(roomId: string) { - this.room = joinRoom({ appId: this.appId }, roomId) - - return this.room + joinRoom() { + this.room = joinRoom({ appId: this.appId }, this.roomId) + /** + * If we wait to join, it will result in not being able to listen to our own join event. + * This might be related to the fact that: + * (If called more than once, only the latest callback registered is ever called.) + * Multiple listeners may overwrite each other. + * @see: https://github.com/dmotz/trystero?tab=readme-ov-file#onpeerjoincallback + */ + // this.room.onPeerJoin(() => this.emit('action')) + this.emit('action') + return this } - async sendMessage(message: T, id?: string) { + sendMessage(message: T, id?: string) { if (!this.room) { - throw new Error('Room not joined') + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + const [send] = this.room.makeAction('MESSAGE') + send(message as DataPayload, id) + }) + } else { + const [send] = this.room.makeAction('MESSAGE') + send(message as DataPayload, id) } - const [send] = this.room!.makeAction('MESSAGE') - return await send(message as DataPayload, id) + + return this } onMessage(callback: (message: T) => void) { if (!this.room) { - throw new Error('Room not joined') + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + const [, on] = this.room.makeAction('MESSAGE') + on((message) => callback(message as T)) + }) + } else { + const [, on] = this.room.makeAction('MESSAGE') + on((message) => callback(message as T)) } - const [, on] = this.room!.makeAction('MESSAGE') - on((message) => callback(message as T)) + return this } onJoinRoom(callback: (id: string) => void) { if (!this.room) { - throw new Error('Room not joined') + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.onPeerJoin((peerId) => { + callback(peerId) + }) + }) + } else { + this.room.onPeerJoin((peerId) => { + callback(peerId) + }) } - this.room.onPeerJoin((peerId) => callback(peerId)) + return this } onLeaveRoom(callback: (id: string) => void) { if (!this.room) { - throw new Error('Room not joined') + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.onPeerLeave((peerId) => callback(peerId)) + }) + } else { + this.room.onPeerLeave((peerId) => callback(peerId)) } - this.room.onPeerLeave((peerId) => callback(peerId)) + return this } - async leaveRoom() { - return await this.room?.leave() + leaveRoom() { + if (!this.room) { + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.leave() + this.room = undefined + }) + } else { + this.room.leave() + this.room = undefined + } + return this + } + + onError(callback: (error: Error) => void) { + this.on('error', (error: Error) => callback(error)) + return this } } -const peerRoom = new PeerRoom({ appId: stringToHex(__NAME__) }) +const hostRoomId = stringToHex(document.location.host) +const peerRoom = new PeerRoom({ roomId: hostRoomId }) export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom) + +// https://github.com/w3c/webextensions/issues/72 +// https://issues.chromium.org/issues/40251342 +// https://github.com/w3c/webrtc-extensions/issues/77 diff --git a/src/domain/impls/PeerRoom2.ts b/src/domain/impls/PeerRoom2.ts new file mode 100644 index 0000000..86f1a2d --- /dev/null +++ b/src/domain/impls/PeerRoom2.ts @@ -0,0 +1,142 @@ +import { Artico, Room } from '@rtco/client' + +import { PeerRoomExtern, type PeerMessage } from '@/domain/externs/PeerRoom' +import { stringToHex } from '@/utils' +import { nanoid } from 'nanoid' +import EventHub from '@resreq/event-hub' +export interface Config { + peerId?: string + roomId: string +} + +class PeerRoom extends EventHub { + readonly roomId: string + private rtco?: Artico + readonly peerId: string + private room?: Room + + constructor(config: Config) { + super() + this.roomId = config.roomId + this.peerId = config.peerId || nanoid() + this.joinRoom = this.joinRoom.bind(this) + this.sendMessage = this.sendMessage.bind(this) + this.onMessage = this.onMessage.bind(this) + this.onJoinRoom = this.onJoinRoom.bind(this) + this.onLeaveRoom = this.onLeaveRoom.bind(this) + this.leaveRoom = this.leaveRoom.bind(this) + this.onError = this.onError.bind(this) + } + + joinRoom() { + if (!this.rtco) { + this.rtco = new Artico({ id: this.peerId }) + } + if (this.room) { + this.room = this.rtco.join(this.roomId) + } else { + this.rtco!.on('open', () => { + this.room = this.rtco!.join(this.roomId) + this.emit('action') + }) + } + return this + } + + sendMessage(message: T, id?: string) { + if (!this.room) { + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.send(JSON.stringify(message), id) + }) + } else { + this.room.send(JSON.stringify(message), id) + } + return this + } + + onMessage(callback: (message: T) => void) { + if (!this.room) { + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.on('message', (message) => callback(JSON.parse(message) as T)) + }) + } else { + this.room.on('message', (message) => callback(JSON.parse(message) as T)) + } + return this + } + + onJoinRoom(callback: (id: string) => void) { + if (!this.room) { + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.on('join', (id) => callback(id)) + }) + } else { + this.room.on('join', (id) => callback(id)) + } + return this + } + + onLeaveRoom(callback: (id: string) => void) { + if (!this.room) { + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.on('leave', (id) => callback(id)) + }) + } else { + this.room.on('leave', (id) => callback(id)) + } + return this + } + + leaveRoom() { + if (!this.room) { + this.once('action', () => { + if (!this.room) { + const error = new Error('Room not joined') + this.emit('error', error) + throw error + } + this.room.leave() + this.room = undefined + }) + } else { + this.room.leave() + this.room = undefined + } + return this + } + onError(callback: (error: Error) => void) { + this.rtco?.on('error', (error) => callback(error)) + this.on('error', (error: Error) => callback(error)) + return this + } +} + +const hostRoomId = stringToHex(document.location.host) + +const peerRoom = new PeerRoom({ roomId: hostRoomId }) + +export const PeerRoomImpl = PeerRoomExtern.impl(peerRoom) + +// https://github.com/w3c/webextensions/issues/72 +// https://issues.chromium.org/issues/40251342 +// https://github.com/w3c/webrtc-extensions/issues/77 diff --git a/src/utils/generateRandomAvatar.ts b/src/utils/generateRandomAvatar.ts index b34c610..dbad10a 100644 --- a/src/utils/generateRandomAvatar.ts +++ b/src/utils/generateRandomAvatar.ts @@ -1,11 +1,11 @@ import generateUglyAvatar from '@/lib/uglyAvatar' import compressImage from './compressImage' -const generateRandomAvatar = async (idealSize: number) => { +const generateRandomAvatar = async (targetSize: number) => { const svgBlob = generateUglyAvatar() // compressImage can't directly compress svg, need to convert to jpeg first - const jpegBlob = await new Promise((resolve, reject) => { + const imageBlob = await new Promise((resolve, reject) => { const image = new Image() image.onload = async () => { const canvas = new OffscreenCanvas(image.width, image.height) @@ -17,7 +17,7 @@ const generateRandomAvatar = async (idealSize: number) => { image.onerror = () => reject(new Error('Failed to load SVG')) image.src = URL.createObjectURL(svgBlob) }) - const miniAvatarBlob = await compressImage({ input: jpegBlob, targetSize: idealSize }) + const miniAvatarBlob = await compressImage({ input: imageBlob, targetSize }) const miniAvatarBase64 = await new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (e) => resolve(e.target?.result as string)