Update task manager to reflect rx-nostr relay connections and auth

This commit is contained in:
hzrd149 2025-01-29 14:24:18 -06:00
parent 41e02e14a5
commit 3d7a5bd13a
90 changed files with 909 additions and 1031 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Update task manager to reflect rx-nostr relay connections and auth

View File

@ -90,6 +90,7 @@
"nostr-idb": "^2.2.0",
"nostr-signer-capacitor-plugin": "^0.0.3",
"nostr-tools": "^2.10.4",
"nostr-typedef": "^0.11.0",
"nostr-wasm": "^0.1.0",
"nuka-carousel": "^8.2.0",
"prettier": "^3.4.2",
@ -118,7 +119,7 @@
"rxjs": "^7.8.1",
"three": "^0.170.0",
"three-spritetext": "^1.9.4",
"three-stdlib": "^2.35.12",
"three-stdlib": "^2.35.13",
"tiny-lru": "^11.2.11",
"unified": "^11.0.5",
"uuid": "^11.0.5",

142
pnpm-lock.yaml generated
View File

@ -104,28 +104,28 @@ importers:
version: 0.7.2
applesauce-accounts:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-content:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-core:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-factory:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-loaders:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-net:
specifier: ^0.10.0
version: 0.10.0(typescript@5.7.3)
applesauce-react:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-signers:
specifier: next
version: 0.0.0-next-20250128190416(typescript@5.7.3)
version: 0.0.0-next-20250129183155(typescript@5.7.3)
bech32:
specifier: ^2.0.0
version: 2.0.0
@ -225,6 +225,9 @@ importers:
nostr-tools:
specifier: ^2.10.4
version: 2.10.4(typescript@5.7.3)
nostr-typedef:
specifier: ^0.11.0
version: 0.11.0
nostr-wasm:
specifier: ^0.1.0
version: 0.1.0
@ -310,8 +313,8 @@ importers:
specifier: ^1.9.4
version: 1.9.4(three@0.170.0)
three-stdlib:
specifier: ^2.35.12
version: 2.35.12(three@0.170.0)
specifier: ^2.35.13
version: 2.35.13(three@0.170.0)
tiny-lru:
specifier: ^11.2.11
version: 11.2.11
@ -2192,32 +2195,32 @@ packages:
engines: {node: '>=8.0.0'}
hasBin: true
applesauce-accounts@0.0.0-next-20250128190416:
resolution: {integrity: sha512-GtfoLOGxey2SlglPJorFCFwHIFuiDJqp5jnsL14Mtm6I1NkZBNHlugyvgH2sMhWuA3CkyOQkpVvS31JczGM/IQ==}
applesauce-accounts@0.0.0-next-20250129183155:
resolution: {integrity: sha512-mO03NJ1uZXj964OW4joh7nBW74dH5keIsDVol9E87FK6kWJEOrkOIS3x1Mi5gvZvllJ2tBRJTEZdeHfV7eCdSQ==}
applesauce-content@0.0.0-next-20250128190416:
resolution: {integrity: sha512-k5hsFzLzWreWK5EaAUhVKRFDT8CbgJfBuZxFK2McBo3Jaxz34YguAdQJKOdBzKcKwTMKtHKsay9BHV2ghckDxQ==}
applesauce-content@0.0.0-next-20250129183155:
resolution: {integrity: sha512-LSRRQ8IsrSp/kycpAZonW0aIuOquRg7SWNwGio8VBsSpH7p/1VuH+gWA1QRzI+Jzb9+uV+1fghYQR7cq/dC2vA==}
applesauce-core@0.0.0-next-20250128190416:
resolution: {integrity: sha512-DCvQHl9TKu+qDdEbUO35SFkC7lyeNvZRs1KNdmUjsQhnhmvuvdJ8s0XJNJvkrSQeHgcXXNzHDU906RxBxqjytA==}
applesauce-core@0.0.0-next-20250129183155:
resolution: {integrity: sha512-B0gKQtpUqJfEcQSDIfJdAAsZTKACS6lzio/OMu34aricEne2XOS+FLeumoMDet9P2oK2MASkbAy2j6eCyFD/7A==}
applesauce-core@0.10.0:
resolution: {integrity: sha512-QMhUh4FIARcqY5soCB4Z8DIu+py0rYb28IgWT4gP9DLBGpDrY8lStXk7W1/46TLjEH97y0hbiXFK7kMCZ31oOQ==}
applesauce-factory@0.0.0-next-20250128190416:
resolution: {integrity: sha512-zYduxpOyk5aswWDoRgdc+90ARQZnQNdOwRnB9VBTB8rxlWzF5v49+rCxcuKUwl/cn5/S2XB5TytDYumCOy5SpQ==}
applesauce-factory@0.0.0-next-20250129183155:
resolution: {integrity: sha512-s6V+HHg+4yG0XELZMsdhUca976teqRhTlf+XCENQC9qW4bAYfkYrP3fZfelVBXhDP4mTzereK6yO2cgH53aQUw==}
applesauce-loaders@0.0.0-next-20250128190416:
resolution: {integrity: sha512-26M/ax+6oVlncREUxEjTneNXYxhfFNrhl94kVJyw78Gx+frmNZLbTts3US3ng+ziKB5lnKZvBJJCXkiev4f9kQ==}
applesauce-loaders@0.0.0-next-20250129183155:
resolution: {integrity: sha512-JxFoSLtJoscdj4D4nUxbjJVk4f5K5Ssjt9gecQQz3XgPAOQasyT8ZkCyhhL+3VkvvT4jYNOMK84kwa9/yEtqlQ==}
applesauce-net@0.10.0:
resolution: {integrity: sha512-ZsAs/MkeGHiPZ2/a8lwP8lx/Eh+5Dot0qG4BLTAqjg4emP/RsiqW+hyc6v6QcVbdvuR0+hP1gka3+wWtiy/cTA==}
applesauce-react@0.0.0-next-20250128190416:
resolution: {integrity: sha512-0jWnZBDDesc7OnT20ZTW3kC/hnHYRwFy2XtOPMpBc2TZAKnAIfQIKnrQqa5YwlCdBo7L/EFL02vz6XnIDS8P+w==}
applesauce-react@0.0.0-next-20250129183155:
resolution: {integrity: sha512-79tWla4qdr5YzZXMIBj0cqbIrY1t7Kr/0kF4BL1Amvx09OYYLVS1rr4YwdXzdEtmCqMr/cotL5K167Fhe6KKMg==}
applesauce-signers@0.0.0-next-20250128190416:
resolution: {integrity: sha512-EDQruPuY+cxSQ6HIRrG2aRDazKUrSvWLH3Ey896/LEqYKwcPMxwzUm2Ppuy+d1GsNJ5O6Nz5txKgfSDRfk2t5Q==}
applesauce-signers@0.0.0-next-20250129183155:
resolution: {integrity: sha512-666Gj7cDThnHR4eT+ExcHo7mN8vWlOJIsEy0FeU+lpFdsZd391AmXSReH0LaV7ja4aHOWqJmQr0M6PkkUKwloQ==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@ -2454,8 +2457,8 @@ packages:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001695:
resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==}
caniuse-lite@1.0.30001696:
resolution: {integrity: sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==}
canvas-color-tracker@1.3.1:
resolution: {integrity: sha512-eNycxGS7oQ3IS/9QQY41f/aQjiO9Y/MtedhCgSdsbLSxC9EyUD8L3ehl/Q3Kfmvt8um79S45PBV+5Rxm5ztdSw==}
@ -4434,6 +4437,9 @@ packages:
typescript:
optional: true
nostr-typedef@0.11.0:
resolution: {integrity: sha512-grXIdS0dnfi3fUQNJFRNKjQZaFl3sYUpQ+U67XRtAFpl/ZAwFWkboJpryv72PS7pQ/Gkd0Q2uEoX8wm9H2uaQg==}
nostr-typedef@0.9.0:
resolution: {integrity: sha512-nLTzhlYcRnLQGUJ5YfvGAUDyGFHjGH6Qozltl/wV3UXelmiUwjrwI8IIxQNkbgVMv+zmbFi/m1xKHxIvVfG09w==}
@ -4715,9 +4721,6 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
quick-lru@4.0.1:
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
engines: {node: '>=8'}
@ -5194,8 +5197,8 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
semver@7.6.3:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
semver@7.7.0:
resolution: {integrity: sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==}
engines: {node: '>=10'}
hasBin: true
@ -5386,8 +5389,8 @@ packages:
resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
engines: {node: '>= 0.10.0'}
streamx@2.21.1:
resolution: {integrity: sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==}
streamx@2.22.0:
resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==}
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
@ -5539,8 +5542,8 @@ packages:
peerDependencies:
three: '>=0.86.0'
three-stdlib@2.35.12:
resolution: {integrity: sha512-3Mb3U7gtf1orCb6j2BBcc8BJsBVoCYUjFtwaq9KM8I7ippz4o9G+aDQdT5AF8Sg5FXXZfnPPccP6ufsP8bgG3g==}
three-stdlib@2.35.13:
resolution: {integrity: sha512-AbXVObkM0OFCKX0r4VmHguGTdebiUQA+Yl+4VNta1wC158gwY86tCkjp2LFfmABtjYJhdK6aP13wlLtxZyLMAA==}
peerDependencies:
three: '>=0.128.0'
@ -6894,7 +6897,7 @@ snapshots:
plist: 3.1.0
prompts: 2.4.2
rimraf: 4.4.1
semver: 7.6.3
semver: 7.7.0
tar: 6.2.1
tslib: 2.6.2
xml2js: 0.5.0
@ -6916,7 +6919,7 @@ snapshots:
plist: 3.1.0
prompts: 2.4.2
rimraf: 4.4.1
semver: 7.6.3
semver: 7.7.0
tar: 6.2.1
tslib: 2.8.1
xml2js: 0.5.0
@ -7169,7 +7172,7 @@ snapshots:
outdent: 0.5.0
prettier: 2.8.8
resolve-from: 5.0.0
semver: 7.6.3
semver: 7.7.0
'@changesets/assemble-release-plan@6.0.5':
dependencies:
@ -7178,7 +7181,7 @@ snapshots:
'@changesets/should-skip-package': 0.1.1
'@changesets/types': 6.0.0
'@manypkg/get-packages': 1.1.3
semver: 7.6.3
semver: 7.7.0
'@changesets/changelog-git@0.2.0':
dependencies:
@ -7211,7 +7214,7 @@ snapshots:
package-manager-detector: 0.2.8
picocolors: 1.1.1
resolve-from: 5.0.0
semver: 7.6.3
semver: 7.7.0
spawndamnit: 3.0.1
term-size: 2.2.1
@ -7234,7 +7237,7 @@ snapshots:
'@changesets/types': 6.0.0
'@manypkg/get-packages': 1.1.3
picocolors: 1.1.1
semver: 7.6.3
semver: 7.7.0
'@changesets/get-release-plan@4.0.6':
dependencies:
@ -8431,10 +8434,10 @@ snapshots:
dependencies:
entities: 2.2.0
applesauce-accounts@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-accounts@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
'@noble/hashes': 1.7.1
applesauce-signers: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-signers: 0.0.0-next-20250129183155(typescript@5.7.3)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
rxjs: 7.8.1
@ -8442,13 +8445,13 @@ snapshots:
- supports-color
- typescript
applesauce-content@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-content@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250129183155(typescript@5.7.3)
mdast-util-find-and-replace: 3.0.2
nostr-tools: 2.10.4(typescript@5.7.3)
remark: 15.0.1
@ -8459,7 +8462,7 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-core@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
'@scure/base': 1.2.4
debug: 4.4.0
@ -8487,19 +8490,19 @@ snapshots:
- supports-color
- typescript
applesauce-factory@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-factory@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
applesauce-content: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-content: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250129183155(typescript@5.7.3)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-loaders@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-loaders@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
applesauce-core: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250129183155(typescript@5.7.3)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
rx-nostr: 3.5.0
@ -8518,12 +8521,12 @@ snapshots:
- supports-color
- typescript
applesauce-react@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-react@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
applesauce-accounts: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-content: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-factory: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-accounts: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-content: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250129183155(typescript@5.7.3)
applesauce-factory: 0.0.0-next-20250129183155(typescript@5.7.3)
nostr-tools: 2.10.4(typescript@5.7.3)
react: 18.3.1
rxjs: 7.8.1
@ -8531,12 +8534,12 @@ snapshots:
- supports-color
- typescript
applesauce-signers@0.0.0-next-20250128190416(typescript@5.7.3):
applesauce-signers@0.0.0-next-20250129183155(typescript@5.7.3):
dependencies:
'@noble/hashes': 1.7.1
'@noble/secp256k1': 1.7.1
'@scure/base': 1.2.4
applesauce-core: 0.0.0-next-20250128190416(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250129183155(typescript@5.7.3)
debug: 4.4.0
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
@ -8651,7 +8654,7 @@ snapshots:
bare-stream@2.6.4(bare-events@2.5.4):
dependencies:
streamx: 2.21.1
streamx: 2.22.0
optionalDependencies:
bare-events: 2.5.4
optional: true
@ -8760,7 +8763,7 @@ snapshots:
browserslist@4.24.4:
dependencies:
caniuse-lite: 1.0.30001695
caniuse-lite: 1.0.30001696
electron-to-chromium: 1.5.88
node-releases: 2.0.19
update-browserslist-db: 1.1.2(browserslist@4.24.4)
@ -8810,7 +8813,7 @@ snapshots:
camelcase@8.0.0: {}
caniuse-lite@1.0.30001695: {}
caniuse-lite@1.0.30001696: {}
canvas-color-tracker@1.3.1:
dependencies:
@ -11081,7 +11084,7 @@ snapshots:
node-abi@3.73.0:
dependencies:
semver: 7.6.3
semver: 7.7.0
node-addon-api@6.1.0: {}
@ -11107,7 +11110,7 @@ snapshots:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.16.1
semver: 7.6.3
semver: 7.7.0
validate-npm-package-license: 3.0.4
nostr-idb@2.2.0(typescript@5.7.3):
@ -11147,6 +11150,8 @@ snapshots:
nostr-wasm: 0.1.0
typescript: 5.7.3
nostr-typedef@0.11.0: {}
nostr-typedef@0.9.0: {}
nostr-wasm@0.1.0: {}
@ -11415,8 +11420,6 @@ snapshots:
queue-microtask@1.2.3: {}
queue-tick@1.0.1: {}
quick-lru@4.0.1: {}
railroad-diagrams@1.0.0: {}
@ -11998,7 +12001,7 @@ snapshots:
semver@6.3.1: {}
semver@7.6.3: {}
semver@7.7.0: {}
send@0.19.0:
dependencies:
@ -12065,7 +12068,7 @@ snapshots:
detect-libc: 2.0.3
node-addon-api: 6.1.0
prebuild-install: 7.1.3
semver: 7.6.3
semver: 7.7.0
simple-get: 4.0.1
tar-fs: 3.0.8
tunnel-agent: 0.6.0
@ -12232,10 +12235,9 @@ snapshots:
stream-buffers@2.2.0: {}
streamx@2.21.1:
streamx@2.22.0:
dependencies:
fast-fifo: 1.3.2
queue-tick: 1.0.1
text-decoder: 1.2.3
optionalDependencies:
bare-events: 2.5.4
@ -12371,7 +12373,7 @@ snapshots:
dependencies:
b4a: 1.6.7
fast-fifo: 1.3.2
streamx: 2.21.1
streamx: 2.22.0
tar@6.2.1:
dependencies:
@ -12443,7 +12445,7 @@ snapshots:
dependencies:
three: 0.170.0
three-stdlib@2.35.12(three@0.170.0):
three-stdlib@2.35.13(three@0.170.0):
dependencies:
'@types/draco3d': 1.4.10
'@types/offscreencanvas': 2019.7.3

View File

@ -1,8 +1,8 @@
import { BehaviorSubject, Subject } from "rxjs";
import { EventTemplate, Relay, VerifiedEvent } from "nostr-tools";
import { ControlMessage, ControlResponse } from "@satellite-earth/core/types";
import { createDefer, Deferred } from "applesauce-core/promise";
import createDefer, { Deferred } from "../deferred";
import { logger } from "../../helpers/debug";
export default class BakeryRelay extends Relay {

View File

@ -4,12 +4,12 @@ import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import { EventStore } from "applesauce-core";
import { getEventUID } from "applesauce-core/helpers";
import { createDefer, Deferred } from "applesauce-core/promise";
import { Subject } from "rxjs";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
import processManager from "../services/process-manager";
import createDefer, { Deferred } from "./deferred";
import Dataflow04 from "../components/icons/dataflow-04";
import SuperMap from "./super-map";

View File

@ -2,12 +2,12 @@ import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import { createDefer, Deferred } from "applesauce-core/promise";
import { Subject } from "rxjs";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
import processManager from "../services/process-manager";
import createDefer, { Deferred } from "./deferred";
import Dataflow04 from "../components/icons/dataflow-04";
import SuperMap from "./super-map";
import { eventStore } from "../services/event-store";

View File

@ -8,9 +8,7 @@ import { Subject } from "rxjs";
import { logger } from "../helpers/debug";
import EventStore from "./event-store";
import deleteEventService from "../services/delete-events";
import { mergeFilter } from "../helpers/nostr/filter";
import { isATag, isETag } from "../types/nostr-event";
import relayPoolService from "../services/relay-pool";
import Process from "./process";
import processManager from "../services/process-manager";
@ -49,9 +47,6 @@ export default class ChunkedRequest {
this.log = log || logger.extend(relay.url);
this.events = new EventStore(relay.url);
// TODO: find a better place for this
this.subs.push(deleteEventService.stream.subscribe((e) => this.handleDeleteEvent(e)));
processManager.registerProcess(this.process);
}
@ -118,14 +113,6 @@ export default class ChunkedRequest {
return this.events.addEvent(event);
}
private handleDeleteEvent(deleteEvent: NostrEvent) {
const cord = deleteEvent.tags.find(isATag)?.[1];
const eventId = deleteEvent.tags.find(isETag)?.[1];
if (cord) this.events.deleteEvent(cord);
if (eventId) this.events.deleteEvent(eventId);
}
getFirstEvent(nth = 0, eventFilter?: EventFilter) {
return this.events.getFirstEvent(nth, eventFilter);
}

View File

@ -1,21 +0,0 @@
export type Deferred<T> = Promise<T> & {
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
};
export default function createDefer<T>() {
let _resolve: (value?: T | PromiseLike<T>) => void;
let _reject: (reason?: any) => void;
const promise = new Promise<T>((resolve, reject) => {
// @ts-ignore
_resolve = resolve;
_reject = reject;
}) as Deferred<T>;
// @ts-ignore
promise.resolve = _resolve;
// @ts-ignore
promise.reject = _reject;
return promise;
}

View File

@ -4,7 +4,6 @@ import { nanoid } from "nanoid";
import { getEventUID, sortByDate } from "../helpers/nostr/event";
import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
import deleteEventService from "../services/delete-events";
export type EventFilter = (event: NostrEvent) => boolean;
@ -19,17 +18,9 @@ export default class EventStore {
customSort?: typeof sortByDate;
private deleteSub: ZenObservable.Subscription;
constructor(name?: string, customSort?: typeof sortByDate) {
this.name = name;
this.customSort = customSort;
this.deleteSub = deleteEventService.stream.subscribe((event) => {
const uid = getEventUID(event);
this.deleteEvent(uid);
if (uid !== event.id) this.deleteEvent(event.id);
});
}
getSortedEvents() {
@ -89,7 +80,6 @@ export default class EventStore {
for (const sub of subs) sub.unsubscribe();
}
this.storeSubs.clear();
this.deleteSub.unsubscribe();
}
getFirstEvent(nth = 0, filter?: EventFilter) {

View File

@ -1,3 +1,4 @@
import { safeParse } from "applesauce-core/helpers";
import { LocalStorageEntry, NullableLocalStorageEntry } from "./entry";
export class NumberLocalStorageEntry extends LocalStorageEntry<number> {
@ -32,3 +33,18 @@ export class BooleanLocalStorageEntry extends LocalStorageEntry<boolean> {
);
}
}
export class ArrayLocalStorageEntry<T extends unknown> extends LocalStorageEntry<T[]> {
constructor(key: string, fallback: T[]) {
super(
key,
fallback,
(raw) => {
const value = safeParse<T[]>(raw);
if (value && Array.isArray(value)) return value;
else return [] as T[];
},
(value) => JSON.stringify(value),
);
}
}

View File

@ -4,7 +4,7 @@ import { Button, Text } from "@chakra-ui/react";
import { getSeenRelays } from "applesauce-core/helpers";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { RelayFavicon } from "../../relay-favicon";
import RelayFavicon from "../../relay-favicon";
export default function DebugEventRelaysPage({ event }: { event: NostrEvent }) {
const publish = usePublishEvent();

View File

@ -0,0 +1,18 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { useTaskManagerContext } from "../../../views/task-manager/provider";
import { connections$ } from "../../../services/rx-nostr";
export default function RelayConnectionButton({ ...props }: Omit<ButtonProps, "children" | "onClick">) {
const { openTaskManager } = useTaskManagerContext();
const connections = useObservable(connections$);
const connected = Object.values(connections).reduce((t, s) => (s === "connected" ? t + 1 : t), 0);
return (
<Button onClick={() => openTaskManager("/relays")} {...props}>
Relays ({connected})
</Button>
);
}

View File

@ -2,13 +2,13 @@ import { useMemo } from "react";
import { Divider, Spacer } from "@chakra-ui/react";
import { useActiveAccount } from "applesauce-react/hooks";
import { ReadonlyAccount } from "applesauce-accounts/accounts";
import { QuestionIcon } from "@chakra-ui/icons";
import { LightningIcon, SettingsIcon } from "../../icons";
import Package from "../../icons/package";
import useRecentIds from "../../../hooks/use-recent-ids";
import { defaultFavoriteApps, internalApps, internalTools } from "../../navigation/apps";
import NavItem from "./nav-item";
import { QuestionIcon } from "@chakra-ui/icons";
import Plus from "../../icons/plus";
import useFavoriteInternalIds from "../../../hooks/use-favorite-internal-ids";
@ -51,7 +51,6 @@ export default function NavItems() {
<Spacer />
<NavItem to="/support" icon={LightningIcon} label="Support" />
<NavItem label="Settings" icon={SettingsIcon} to="/settings" />
{/* <TaskManagerButtons mt="auto" flexShrink={0} /> */}
</>
);
}

View File

@ -0,0 +1,28 @@
import { useContext } from "react";
import { Button, ButtonProps } from "@chakra-ui/react";
import { PublishContext, PublishLogEntry } from "../../../providers/global/publish-provider";
import { useTaskManagerContext } from "../../../views/task-manager/provider";
import { usePublishLogEntryStatus } from "../../../views/task-manager/publish-log/action-status-tag";
function PublishLogEntryButton({ entry, ...props }: Omit<ButtonProps, "children"> & { entry: PublishLogEntry }) {
const { openTaskManager } = useTaskManagerContext();
const { icon, color, successful, total } = usePublishLogEntryStatus(entry);
return (
<Button onClick={() => openTaskManager("/publish-log")} colorScheme={color} {...props}>
{successful.length}/{total}
</Button>
);
}
export default function PublishLogButton({ ...props }: Omit<ButtonProps, "children" | "onClick">) {
const { log } = useContext(PublishContext);
const { openTaskManager } = useTaskManagerContext();
const entry = log[log.length - 1];
if (!entry) return null;
return <PublishLogEntryButton entry={entry} onClick={() => openTaskManager("/publish-log")} {...props} />;
}

View File

@ -1,11 +1,13 @@
import { useState } from "react";
import { Flex, FlexProps, IconButton } from "@chakra-ui/react";
import { ButtonGroup, Flex, FlexProps, IconButton } from "@chakra-ui/react";
import { ChevronLeftIcon, ChevronRightIcon } from "../../icons";
import NavItems from "../nav-items";
import NavItems from "../components";
import useRootPadding from "../../../hooks/use-root-padding";
import AccountSwitcher from "../nav-items/account-switcher";
import AccountSwitcher from "../components/account-switcher";
import { CollapsedContext } from "../context";
import RelayConnectionButton from "../components/connections-button";
import PublishLogButton from "../components/publish-log-button";
export default function DesktopSideNav({ ...props }: Omit<FlexProps, "children">) {
const [collapsed, setCollapsed] = useState(false);
@ -36,14 +38,20 @@ export default function DesktopSideNav({ ...props }: Omit<FlexProps, "children">
>
<AccountSwitcher />
<NavItems />
<IconButton
aria-label={collapsed ? "Open" : "Close"}
title={collapsed ? "Open" : "Close"}
size="sm"
variant="ghost"
onClick={() => setCollapsed(!collapsed)}
icon={collapsed ? <ChevronRightIcon boxSize={6} /> : <ChevronLeftIcon boxSize={6} />}
/>
<ButtonGroup variant="ghost">
<IconButton
aria-label={collapsed ? "Open" : "Close"}
title={collapsed ? "Open" : "Close"}
onClick={() => setCollapsed(!collapsed)}
icon={collapsed ? <ChevronRightIcon boxSize={6} /> : <ChevronLeftIcon boxSize={6} />}
/>
{!collapsed && (
<>
<RelayConnectionButton w="full" />
<PublishLogButton flexShrink={0} />
</>
)}
</ButtonGroup>
</Flex>
</CollapsedContext.Provider>
);

View File

@ -2,13 +2,17 @@ import { Outlet } from "react-router-dom";
import MobileBottomNav from "./bottom-nav";
import { ErrorBoundary } from "../../error-boundary";
import { Suspense } from "react";
import { Spinner } from "@chakra-ui/react";
export default function MobileLayout() {
return (
<>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
<Suspense fallback={<Spinner />}>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</Suspense>
<MobileBottomNav />
</>
);

View File

@ -1,19 +1,47 @@
import { Avatar, Drawer, DrawerBody, DrawerContent, DrawerOverlay, DrawerProps, Flex, Text } from "@chakra-ui/react";
import {
Avatar,
ButtonGroup,
Drawer,
DrawerBody,
DrawerContent,
DrawerOverlay,
DrawerProps,
Flex,
Text,
} from "@chakra-ui/react";
import { useActiveAccount } from "applesauce-react/hooks";
import AccountSwitcher from "../nav-items/account-switcher";
import NavItems from "../nav-items";
import AccountSwitcher from "../components/account-switcher";
import NavItems from "../components";
import { CollapsedContext } from "../context";
import RelayConnectionButton from "../components/connections-button";
import PublishLogButton from "../components/publish-log-button";
import { MouseEventHandler } from "react";
export default function NavDrawer({ ...props }: Omit<DrawerProps, "children">) {
export default function NavDrawer({ onClose, ...props }: Omit<DrawerProps, "children">) {
const account = useActiveAccount();
const handleClickItem: MouseEventHandler = (e) => {
if (e.target instanceof HTMLAnchorElement || e.target instanceof HTMLButtonElement) {
onClose();
}
};
return (
<Drawer placement="left" {...props}>
<Drawer placement="left" onClose={onClose} {...props}>
<DrawerOverlay />
<DrawerContent>
<CollapsedContext.Provider value={false}>
<DrawerBody display="flex" flexDirection="column" px="4" pt="4" overflowY="auto" overflowX="hidden" gap="2">
<DrawerBody
display="flex"
flexDirection="column"
px="4"
pt="4"
overflowY="auto"
overflowX="hidden"
gap="2"
onClick={handleClickItem}
>
{!account && (
<Flex gap="2" my="2" alignItems="center">
<Avatar src="/apple-touch-icon.png" size="md" />
@ -22,6 +50,10 @@ export default function NavDrawer({ ...props }: Omit<DrawerProps, "children">) {
)}
<AccountSwitcher />
<NavItems />
<ButtonGroup variant="ghost" onClick={onClose}>
<RelayConnectionButton w="full" />
<PublishLogButton flexShrink={0} />
</ButtonGroup>
</DrawerBody>
</CollapsedContext.Provider>
</DrawerContent>

View File

@ -1,113 +0,0 @@
import { Code, Flex, FlexProps, LinkBox, Text } from "@chakra-ui/react";
import { NostrEvent, kinds, nip19, nip25 } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import { useActiveAccount } from "applesauce-react/hooks";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import TimelineActionAndStatus from "../../timeline/timeline-action-and-status";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import Timestamp from "../../timestamp";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import { getDMRecipient, getDMSender } from "../../../helpers/nostr/dms";
import UserName from "../../user/user-name";
import HoverLinkOverlay from "../../hover-link-overlay";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import { getSharableEventAddress } from "../../../services/relay-hints";
const kindColors: Record<number, FlexProps["bg"]> = {
[kinds.ShortTextNote]: "blue.500",
[kinds.EncryptedDirectMessage]: "orange.500",
[kinds.Repost]: "yellow.500",
[kinds.GenericRepost]: "yellow.500",
[kinds.Reaction]: "green.500",
[kinds.LongFormArticle]: "purple.500",
};
function KindTag({ event }: { event: NostrEvent }) {
return (
<Code
px="2"
fontFamily="monospace"
fontWeight="bold"
borderLeftWidth={4}
borderLeftColor={kindColors[event.kind] || "gray.500"}
fontSize="md"
>
{event.kind}
</Code>
);
}
function TimelineItem({ event }: { event: NostrEvent }) {
const ref = useEventIntersectionRef(event);
const renderContent = () => {
switch (event.kind) {
case kinds.EncryptedDirectMessage: {
const sender = getDMSender(event);
const recipient = getDMRecipient(event);
return (
<Text>
<UserName pubkey={sender} fontWeight="bold" /> messaged <UserName pubkey={recipient} fontWeight="bold" />
</Text>
);
}
case kinds.Contacts: {
return (
<Text noOfLines={1} isTruncated>
Updated contacts
</Text>
);
}
case kinds.Reaction: {
const pointer = nip25.getReactedEventPointer(event);
return (
<HoverLinkOverlay
as={RouterLink}
to={`/l/${pointer ? nip19.neventEncode(pointer) : ""}`}
noOfLines={1}
isTruncated
>
{event.content}
</HoverLinkOverlay>
);
}
default: {
return (
<HoverLinkOverlay as={RouterLink} to={`/l/${getSharableEventAddress(event)}`} noOfLines={1} isTruncated>
{event.content}
</HoverLinkOverlay>
);
}
}
};
return (
<Flex as={LinkBox} ref={ref} gap="2" py="1" overflow="hidden" flexShrink={0}>
<KindTag event={event} />
{renderContent()}
<Timestamp timestamp={event.created_at} ml="auto" />
</Flex>
);
}
export default function GhostTimeline({ ...props }: Omit<FlexProps, "children">) {
const account = useActiveAccount()!;
const readRelays = useReadRelays();
const { loader, timeline: events } = useTimelineLoader(`${account.pubkey}-ghost`, readRelays, {
authors: [account.pubkey],
});
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" overflow="auto" {...props}>
{events?.map((event) => <TimelineItem key={event.id} event={event} />)}
<TimelineActionAndStatus timeline={loader} />
</Flex>
</IntersectionObserverProvider>
);
}

View File

@ -1,46 +0,0 @@
import { useContext } from "react";
import { Button, Flex, FlexProps } from "@chakra-ui/react";
import { PublishContext } from "../../providers/global/publish-provider";
import { useTaskManagerContext } from "../../views/task-manager/provider";
import PublishActionStatusTag from "../../views/task-manager/publish-log/action-status-tag";
import PasscodeLock from "../icons/passcode-lock";
import relayPoolService from "../../services/relay-pool";
export default function TaskManagerButtons({ ...props }: Omit<FlexProps, "children">) {
const { log } = useContext(PublishContext);
const { openTaskManager } = useTaskManagerContext();
const pendingAuth = Array.from(relayPoolService.challenges.entries()).filter(
([r, c]) => r.connected && !!c.value && !relayPoolService.authenticated.get(r).value,
);
return (
<Flex gap="2" {...props}>
<Button
justifyContent="space-between"
onClick={() => openTaskManager(log.length === 0 ? "/relays" : "/publish-log")}
py="2"
variant="link"
w="full"
>
Task Manager
{log.length > 0 && <PublishActionStatusTag entry={log[log.length - 1]} />}
</Button>
{pendingAuth.length > 0 && (
<Button
leftIcon={<PasscodeLock boxSize={5} />}
aria-label="Pending Auth"
title="Pending Auth"
ml="auto"
size="sm"
variant="ghost"
color="red"
onClick={() => openTaskManager({ pathname: "/relays", search: "?tab=auth" })}
>
{pendingAuth.length}
</Button>
)}
</Flex>
);
}

View File

@ -26,7 +26,7 @@ import UserLink from "./user/user-link";
import relayPoolService from "../services/relay-pool";
import { isValidRelayURL } from "../helpers/relay";
import relayScoreboardService from "../services/relay-scoreboard";
import { RelayFavicon } from "./relay-favicon";
import RelayFavicon from "./relay-favicon";
import singleEventLoader from "../services/single-event-loader";
import replaceableEventLoader from "../services/replaceable-loader";
import { AppHandlerContext } from "../providers/route/app-handler-provider";

View File

@ -3,12 +3,16 @@ import { Avatar, AvatarProps } from "@chakra-ui/react";
import { RelayIcon } from "./icons";
import { useRelayInfo } from "../hooks/use-relay-info";
import useRelayConnectionState from "../hooks/use-relay-connection-state";
import { getConnectionStateColor } from "../helpers/relay";
export type RelayFaviconProps = Omit<AvatarProps, "src"> & {
relay: string;
};
export const RelayFavicon = React.memo(({ relay, ...props }: RelayFaviconProps) => {
const RelayFavicon = React.memo(({ relay, showStatus, ...props }: RelayFaviconProps & { showStatus?: boolean }) => {
const { info } = useRelayInfo(relay);
const state = useRelayConnectionState(relay);
const color = getConnectionStateColor(state);
const url = useMemo(() => {
if (info?.icon) return info.icon;
@ -19,6 +23,18 @@ export const RelayFavicon = React.memo(({ relay, ...props }: RelayFaviconProps)
return url.toString();
}, [relay, info]);
return <Avatar src={url} icon={<RelayIcon />} overflow="hidden" {...props} />;
return (
<Avatar
src={url}
icon={<RelayIcon />}
overflow="hidden"
colorScheme={color}
outline={showStatus ? "2px solid" : "none"}
outlineColor={showStatus ? color + ".500" : undefined}
{...props}
/>
);
});
RelayFavicon.displayName = "RelayFavicon";
export default RelayFavicon;

View File

@ -14,7 +14,7 @@ import {
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { RelayFavicon } from "./relay-favicon";
import RelayFavicon from "./relay-favicon";
import relayScoreboardService from "../services/relay-scoreboard";
export type RelayIconStackProps = { relays: string[]; maxRelays?: number } & Omit<FlexProps, "children">;

View File

@ -1,180 +0,0 @@
import { useMemo, useState } from "react";
import {
Button,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
DrawerProps,
Flex,
IconButton,
Link,
Select,
useDisclosure,
} from "@chakra-ui/react";
import { CloseIcon } from "@chakra-ui/icons";
import { Link as RouterLink } from "react-router-dom";
import { useActiveAccount, useObservable } from "applesauce-react/hooks";
import { NostrEvent } from "nostr-tools";
import { useReadRelays, useWriteRelays } from "../../hooks/use-client-relays";
import relayPoolService from "../../services/relay-pool";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import RelaySet from "../../classes/relay-set";
import UploadCloud01 from "../icons/upload-cloud-01";
import { RelayFavicon } from "../relay-favicon";
import useUserRelaySets from "../../hooks/use-user-relay-sets";
import { getListName } from "../../helpers/nostr/lists";
import { getEventCoordinate } from "../../helpers/nostr/event";
import AddRelayForm from "../../views/relays/app/add-relay-form";
import { SaveRelaySetForm } from "./save-relay-set-form";
function RelayControl({ url }: { url: string }) {
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
const writeRelays = useObservable(clientRelaysService.writeRelays);
const color = relay.connected ? "green" : "red";
const onChange = () => {
if (writeRelays.has(url)) clientRelaysService.removeRelay(url, RelayMode.WRITE);
else clientRelaysService.addRelay(url, RelayMode.WRITE);
};
return (
<Flex gap="2" alignItems="center">
<RelayFavicon relay={url} size="xs" outline="2px solid" outlineColor={color} />
<Link as={RouterLink} to={`/r/${encodeURIComponent(url)}`} isTruncated>
{url}
</Link>
<IconButton
ml="auto"
aria-label="Toggle Write"
icon={<UploadCloud01 />}
size="sm"
variant={writeRelays.has(url) ? "solid" : "ghost"}
colorScheme={writeRelays.has(url) ? "green" : "gray"}
onClick={onChange}
title="Toggle Write"
/>
<IconButton
aria-label="Remove Relay"
icon={<CloseIcon />}
size="sm"
colorScheme="red"
onClick={() => clientRelaysService.removeRelay(url, RelayMode.ALL)}
/>
</Flex>
);
}
function SelectRelaySet({
value,
onChange,
relaySets,
}: {
relaySets: NostrEvent[];
value?: string;
onChange: (cord: string) => void;
}) {
return (
<Select
size="sm"
borderRadius="md"
placeholder="Custom Relays"
value={value}
onChange={(e) => onChange(e.target.value)}
>
{relaySets.map((set) => (
<option key={set.id} value={getEventCoordinate(set)}>
{getListName(set)}
</option>
))}
</Select>
);
}
export default function RelayManagementDrawer({ isOpen, onClose, ...props }: Omit<DrawerProps, "children">) {
const account = useActiveAccount();
const readRelays = useReadRelays();
const writeRelays = useWriteRelays();
const sorted = useMemo(() => RelaySet.from(readRelays, writeRelays).urls.sort(), [readRelays, writeRelays]);
const others = Array.from(relayPoolService.relays.values())
.filter((r) => !r.connected && !sorted.includes(r.url))
.map((r) => r.url)
.sort();
const save = useDisclosure();
const [selected, setSelected] = useState<string>();
const relaySets = useUserRelaySets(account?.pubkey) ?? [];
const changeSet = (cord: string) => {
setSelected(cord);
const set = relaySets.find((s) => getEventCoordinate(s) === cord);
if (set) {
clientRelaysService.setRelaysFromRelaySet(set);
}
};
const renderContent = () => {
if (save.isOpen) {
return (
<>
<SaveRelaySetForm
relaySet={relaySets.find((s) => getEventCoordinate(s) === selected)}
onCancel={save.onClose}
onSaved={(set) => {
save.onClose();
setSelected(getEventCoordinate(set));
}}
writeRelays={clientRelaysService.writeRelays.value}
readRelays={clientRelaysService.readRelays.value}
/>
</>
);
}
return (
<>
<Flex gap="2">
<SelectRelaySet relaySets={relaySets} value={selected} onChange={changeSet} />
<Button size="sm" colorScheme="primary" onClick={save.onOpen}>
Save
</Button>
</Flex>
{sorted.map((url) => (
<RelayControl key={url} url={url} />
))}
<AddRelayForm
onSubmit={(url) => {
clientRelaysService.addRelay(url, RelayMode.ALL);
setSelected(undefined);
}}
/>
{/* <Heading size="sm">Other Relays</Heading>
{others.map((url) => (
<RelayControl key={url} url={url} />
))} */}
</>
);
};
return (
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="md" closeOnEsc={false} {...props}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader px="4" py="2">
Relays
</DrawerHeader>
<DrawerBody px={{ base: 2, md: 4 }} pb="2" pt="0" display="flex" gap="2" flexDir="column">
{renderContent()}
</DrawerBody>
</DrawerContent>
</Drawer>
);
}

View File

@ -1,65 +0,0 @@
import { Button, Flex, FormControl, FormLabel, Input, Textarea } from "@chakra-ui/react";
import { NostrEvent, kinds } from "nostr-tools";
import { useForm } from "react-hook-form";
import { getListDescription, getListName, setListDescription, setListName } from "../../helpers/nostr/lists";
import { isRTag } from "../../types/nostr-event";
import { cloneEvent, ensureDTag } from "../../helpers/nostr/event";
import { createRTagsFromRelaySets } from "../../helpers/nostr/mailbox";
import { usePublishEvent } from "../../providers/global/publish-provider";
export function SaveRelaySetForm({
relaySet,
onCancel,
onSaved,
writeRelays,
readRelays,
}: {
relaySet?: NostrEvent;
onCancel: () => void;
onSaved?: (event: NostrEvent) => void;
writeRelays: Iterable<string>;
readRelays: Iterable<string>;
}) {
const publish = usePublishEvent();
const { register, formState, handleSubmit } = useForm({
defaultValues: {
name: relaySet ? (getListName(relaySet) ?? "") : "",
description: relaySet ? (getListDescription(relaySet) ?? "") : "",
},
mode: "all",
resetOptions: { keepDirtyValues: true },
});
const submit = handleSubmit(async (values) => {
const draft = cloneEvent(kinds.Relaysets, relaySet);
ensureDTag(draft);
setListName(draft, values.name);
setListDescription(draft, values.description);
draft.tags = draft.tags.filter((t) => !isRTag(t));
draft.tags.push(...createRTagsFromRelaySets(readRelays, writeRelays));
const pub = await publish("Save Relay Set", draft);
if (pub && onSaved) onSaved(pub.event);
});
return (
<Flex as="form" onSubmit={submit} direction="column" gap="2">
<FormControl isInvalid={!!formState.errors.name} isRequired>
<FormLabel>Name</FormLabel>
<Input type="text" {...register("name", { required: true })} isRequired autoComplete="off" />
</FormControl>
<FormControl isInvalid={!!formState.errors.description}>
<FormLabel>Description</FormLabel>
<Textarea {...register("description")} />
</FormControl>
<Flex justifyContent="space-between">
<Button onClick={onCancel}>Cancel</Button>
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
Save
</Button>
</Flex>
</Flex>
);
}

View File

@ -1,76 +0,0 @@
import { useCallback, useState } from "react";
import { IconButton, IconButtonProps, useInterval, useToast } from "@chakra-ui/react";
import { type AbstractRelay } from "nostr-tools/abstract-relay";
import { useObservable } from "applesauce-react/hooks";
import relayPoolService from "../../services/relay-pool";
import { useSigningContext } from "../../providers/global/signing-provider";
import PasscodeLock from "../icons/passcode-lock";
import CheckCircleBroken from "../icons/check-circle-broken";
import useForceUpdate from "../../hooks/use-force-update";
export function useRelayChallenge(relay: AbstractRelay) {
return useObservable(relayPoolService.challenges.get(relay));
}
export function useRelayAuthMethod(relay: AbstractRelay) {
const toast = useToast();
const { requestSignature } = useSigningContext();
const challenge = useRelayChallenge(relay);
const authenticated = useObservable(relayPoolService.authenticated.get(relay));
const [loading, setLoading] = useState(false);
const auth = useCallback(async () => {
setLoading(true);
try {
const message = await relayPoolService.authenticate(relay, requestSignature, false);
toast({ description: message || "Success", status: "success" });
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
setLoading(false);
}, [relay, requestSignature]);
return { loading, auth, challenge, authenticated };
}
export function IconRelayAuthButton({
relay,
...props
}: { relay: string | URL | AbstractRelay } & Omit<IconButtonProps, "icon" | "aria-label" | "title">) {
const r = relayPoolService.getRelay(relay);
if (!r) return null;
const update = useForceUpdate();
useInterval(update, 500);
const { challenge, auth, loading, authenticated } = useRelayAuthMethod(r);
if (authenticated) {
return (
<IconButton
icon={<CheckCircleBroken boxSize={6} />}
aria-label="Authenticated"
title="Authenticated"
colorScheme="green"
{...props}
/>
);
}
if (r.connected && challenge) {
return (
<IconButton
icon={<PasscodeLock boxSize={6} />}
onClick={auth}
isLoading={loading}
aria-label="Authenticate with relay"
title="Authenticate"
{...props}
/>
);
}
return null;
}

View File

@ -0,0 +1,43 @@
import { Badge, Box, Flex, Link, Spacer } from "@chakra-ui/react";
import useRelayAuthState from "../../hooks/use-relay-auth-state";
import RelayFavicon from "../relay-favicon";
import RelayAuthModeSelect from "./relay-auth-mode-select";
import { RelayAuthIconButton } from "./relay-auth-icon-button";
import RouterLink from "../router-link";
export default function RelayAuthCard({ relay }: { relay: string }) {
const state = useRelayAuthState(relay);
let badgeColor = "gray";
switch (state?.status) {
case "signing":
badgeColor = "blue";
break;
case "requested":
badgeColor = "orange";
break;
case "rejected":
badgeColor = "red";
break;
case "success":
badgeColor = "green";
break;
}
return (
<Flex gap="2" p="2" alignItems="center" borderWidth={1} rounded="md">
<RelayFavicon relay={relay} size="sm" mx="2" showStatus />
<Flex direction="column" overflow="hidden" alignItems="flex-start">
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay)}`} fontWeight="bold" isTruncated>
{relay}
</Link>
<Badge colorScheme={badgeColor}>{state?.status}</Badge>
</Flex>
<Spacer />
<RelayAuthIconButton relay={relay} variant="ghost" flexShrink={0} />
<RelayAuthModeSelect size="sm" w="auto" rounded="md" flexShrink={0} relay={relay} />
</Flex>
);
}

View File

@ -0,0 +1,52 @@
import { useCallback } from "react";
import { IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
import PasscodeLock from "../icons/passcode-lock";
import authenticationSigner from "../../services/authentication-signer";
import useRelayAuthState from "../../hooks/use-relay-auth-state";
import CheckCircleBroken from "../icons/check-circle-broken";
export function RelayAuthIconButton({
relay,
...props
}: { relay: string } & Omit<IconButtonProps, "icon" | "aria-label" | "title">) {
const toast = useToast();
const authState = useRelayAuthState(relay);
const authenticate = useCallback(async () => {
try {
await authenticationSigner.authenticate(relay);
toast({ description: "Success", status: "success" });
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
}, [relay]);
switch (authState?.status) {
case "success":
return (
<IconButton
icon={<CheckCircleBroken boxSize={6} />}
aria-label="Authenticated"
title="Authenticated"
colorScheme="green"
{...props}
/>
);
case "signing":
case "requested":
return (
<IconButton
icon={<PasscodeLock boxSize={6} />}
onClick={authenticate}
isLoading={authState.status === "signing"}
aria-label="Authenticate with relay"
title="Authenticate"
{...props}
/>
);
default:
return null;
}
}

View File

@ -0,0 +1,36 @@
import { Select, SelectProps } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { RelayAuthMode } from "../../classes/relay-pool";
import localSettings from "../../services/local-settings";
export default function RelayAuthModeSelect({
relay,
...props
}: { relay: string } & Omit<SelectProps, "value" | "onChange" | "children">) {
const defaultMode = useObservable(localSettings.defaultAuthenticationMode);
const relayMode = useObservable(localSettings.relayAuthenticationMode);
const authMode = relayMode.find((r) => r.relay === relay)?.mode ?? "";
const setAuthMode = (mode: RelayAuthMode | "") => {
const existing = relayMode.find((r) => r.relay === relay);
if (!mode) {
if (existing) localSettings.relayAuthenticationMode.next(relayMode.filter((r) => r.relay !== relay));
} else {
if (existing)
localSettings.relayAuthenticationMode.next(relayMode.map((r) => (r.relay === relay ? { relay, mode } : r)));
else localSettings.relayAuthenticationMode.next([...relayMode, { relay, mode }]);
}
};
return (
<Select value={authMode} onChange={(e) => setAuthMode(e.target.value as RelayAuthMode)} {...props}>
<option value="">Default ({defaultMode})</option>
<option value="always">Always</option>
<option value="ask">Ask</option>
<option value="never">Never</option>
</Select>
);
}

View File

@ -1,38 +1,19 @@
import { Badge } from "@chakra-ui/react";
import { useInterval } from "react-use";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { Badge, BadgeProps } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import relayPoolService from "../../services/relay-pool";
import useForceUpdate from "../../hooks/use-force-update";
import { connections$ } from "../../services/rx-nostr";
import { getConnectionStateColor } from "../../helpers/relay";
const getStatusText = (relay: AbstractRelay, connecting = false) => {
if (connecting) return "Connecting...";
if (relay.connected) return "Connected";
// if (relay.closing) return "Disconnecting...";
// if (relay.closed) return "Disconnected";
return "Disconnected";
// return "Unused";
};
const getStatusColor = (relay: AbstractRelay, connecting = false) => {
if (connecting) return "yellow";
if (relay.connected) return "green";
// if (relay.closing) return "yellow";
// if (relay.closed) return "red";
// return "gray";
return "red";
};
export default function RelayStatusBadge({
relay,
...props
}: { relay: string } & Omit<BadgeProps, "colorScheme" | "children">) {
const connections = useObservable(connections$);
const state = connections[relay];
export const RelayStatus = ({ url, relay }: { url?: string; relay?: AbstractRelay }) => {
const update = useForceUpdate();
useInterval(() => update(), 500);
if (!relay) {
if (url) relay = relayPoolService.getRelay(url);
else throw Error("Missing url or relay");
}
const connecting = useObservable(relayPoolService.connecting.get(relay!));
return <Badge colorScheme={getStatusColor(relay!, connecting)}>{getStatusText(relay!, connecting)}</Badge>;
};
return (
<Badge colorScheme={getConnectionStateColor(state)} {...props}>
{state}
</Badge>
);
}

View File

@ -18,7 +18,7 @@ import { getSeenRelays } from "applesauce-core/helpers";
import TimelineLoader from "../../../classes/timeline-loader";
import { NostrEvent } from "../../../types/nostr-event";
import { RelayFavicon } from "../../relay-favicon";
import RelayFavicon from "../../relay-favicon";
import { NoteLink } from "../../note/note-link";
import { BroadcastEventIcon } from "../../icons";
import Timestamp from "../../timestamp";

View File

@ -1,6 +1,8 @@
import { ThemeTypings } from "@chakra-ui/react";
import { Filter } from "nostr-tools";
import { SubCloser, SubscribeManyParams } from "nostr-tools/abstract-pool";
import { AbstractRelay, Subscription } from "nostr-tools/abstract-relay";
import { ConnectionState } from "rx-nostr";
// NOTE: only use this for equality checks and querying
export function getRelayVariations(relay: string) {
@ -172,3 +174,29 @@ export function subscribeMany(relays: string[], filters: Filter[], params: Subsc
},
};
}
export function getConnectionStateColor(state: ConnectionState): ThemeTypings["colorSchemes"] {
switch (state) {
case "initialized":
case "connecting":
return "blue";
case "connected":
return "green";
case "rejected":
case "error":
return "red";
case "waiting-for-retrying":
return "orange";
case "retrying":
return "yellow";
default:
case "dormant":
case "terminated":
return "gray";
}
}

View File

@ -0,0 +1,7 @@
import { useObservable } from "applesauce-react/hooks";
import authenticationSigner, { RelayAuthState } from "../services/authentication-signer";
export default function useRelayAuthState(relay: string): RelayAuthState | undefined {
const states = useObservable(authenticationSigner.relayState$);
return states[relay];
}

View File

@ -0,0 +1,7 @@
import { useObservable } from "applesauce-react/hooks";
import { connections$ } from "../services/rx-nostr";
export default function useRelayConnectionState(relay: string) {
const connections = useObservable(connections$);
return connections[relay];
}

View File

@ -0,0 +1,6 @@
import { useObservable } from "applesauce-react/hooks";
import { notices$ } from "../services/rx-nostr";
export default function useRelayNotices(relay: string) {
return useObservable(notices$).filter((n) => n.from === relay);
}

View File

@ -13,7 +13,6 @@ import clientRelaysService from "../../services/client-relays";
import RelaySet from "../../classes/relay-set";
import { getAllRelayHints } from "../../helpers/nostr/event";
import { getCacheRelay } from "../../services/cache-relay";
import deleteEventService from "../../services/delete-events";
import { eventStore } from "../../services/event-store";
import { useUserOutbox } from "../../hooks/use-user-mailboxes";
import rxNostr from "../../services/rx-nostr";
@ -34,7 +33,11 @@ export class PublishLogEntry extends BehaviorSubject<PublishResults> {
) {
super({ packets: [], relays: {} });
rxNostr.send(event, { on: { relays: [...relays] } }).subscribe({
const defaultWriteRelays = Array.from(Object.entries(rxNostr.getDefaultRelays()))
.filter(([_, config]) => config.write)
.map(([relay]) => relay);
rxNostr.send(event, { on: { relays: [...defaultWriteRelays, ...relays] } }).subscribe({
next: (packet) => {
if (packet.ok) {
addSeenRelay(event, packet.from);
@ -142,9 +145,8 @@ export default function PublishProvider({ children }: PropsWithChildren) {
const cacheRelay = getCacheRelay();
if (cacheRelay) cacheRelay.publish(signed);
// pass it to other services
// add it to the event store
eventStore.add(signed);
if (signed.kind === kinds.EventDeletion) deleteEventService.handleEvent(signed);
return entry;
} catch (e) {

View File

@ -21,10 +21,9 @@ import {
} from "@chakra-ui/react";
import { Event, kinds } from "nostr-tools";
import { useActiveAccount } from "applesauce-react/hooks";
import dayjs from "dayjs";
import { createDefer, Deferred } from "applesauce-core/promise";
import createDefer, { Deferred } from "../../classes/deferred";
import { RelayFavicon } from "../../components/relay-favicon";
import RelayFavicon from "../../components/relay-favicon";
import { ExternalLinkIcon } from "../../components/icons";
import { getEventCoordinate, isReplaceable } from "../../helpers/nostr/event";
import { Tag } from "../../types/nostr-event";
@ -33,6 +32,7 @@ import { useWriteRelays } from "../../hooks/use-client-relays";
import { usePublishEvent } from "../global/publish-provider";
import { useUserOutbox } from "../../hooks/use-user-mailboxes";
import { eventStore } from "../../services/event-store";
import { unixNow } from "applesauce-core/helpers";
type DeleteEventContextType = {
isLoading: boolean;
@ -80,7 +80,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
kind: kinds.EventDeletion,
tags,
content: reason,
created_at: dayjs().unix(),
created_at: unixNow(),
};
const pub = await publish("Delete", draft, undefined, false);
eventStore.add(pub.event);

View File

@ -1,6 +1,7 @@
import React, { useCallback, useContext, useState } from "react";
import { createDefer, Deferred } from "applesauce-core/promise";
import InvoiceModal from "../../components/invoice-modal";
import createDefer, { Deferred } from "../../classes/deferred";
import useAppSettings from "../../hooks/use-user-app-settings";
export type InvoiceModalContext = {

View File

@ -0,0 +1,200 @@
import { Nip07Interface } from "applesauce-signers";
import { EventTemplate, NostrEvent } from "nostr-tools";
import { ConnectionState, EventSigner } from "rx-nostr";
import { BehaviorSubject } from "rxjs";
import { createDefer, Deferred } from "applesauce-core/promise";
import { unixNow } from "applesauce-core/helpers";
import * as Nostr from "nostr-typedef";
import accounts from "./accounts";
import { logger } from "../helpers/debug";
import localSettings from "./local-settings";
export type RelayAuthMode = "always" | "ask" | "never";
type HasChallenge = { template: EventTemplate; challenge: string };
export type RelayAuthDormantState = { status: "dormant" };
export type RelayAuthRequestedState = { status: "requested"; promise: Deferred<NostrEvent> } & HasChallenge;
export type RelayAuthSigningState = { status: "signing"; promise: Deferred<NostrEvent> };
export type RelayAuthRejectedState = { status: "rejected"; reason: string };
export type RelayAuthSuccessState = { status: "success" };
export type RelayAuthState =
| RelayAuthDormantState
| RelayAuthRequestedState
| RelayAuthSigningState
| RelayAuthRejectedState
| RelayAuthSuccessState;
class AuthenticationSigner implements EventSigner {
protected log = logger.extend("AuthenticationSigner");
relayState$ = new BehaviorSubject<Record<string, RelayAuthState>>({});
get relayState() {
return this.relayState$.value;
}
protected get signer() {
if (this.upstream instanceof BehaviorSubject) return this.upstream.value;
else return this.upstream;
}
constructor(protected upstream: Nip07Interface | BehaviorSubject<Nip07Interface | undefined>) {}
defaultMode: RelayAuthMode = "ask";
relayMode = new Map<string, RelayAuthMode>();
/** manually sign an authenticate request */
authenticate(relay: string) {
const state = this.getRelayState(relay);
if (state?.status === "signing") return state.promise;
// TODO: maybe throw here?
if (state?.status !== "requested") return;
const signer = this.signer;
if (!signer) throw new Error("Missing signer");
const log = this.log.extend(relay);
log(`Requesting signature`);
const promise = createDefer<NostrEvent>();
this.setRelayState(relay, { status: "signing", promise });
// update status after signing is complete
const request = state.promise;
promise.then(
(event) => {
log(`Authenticated with ${relay}`);
this.setRelayState(relay, { status: "success" });
request.resolve(event);
},
(err) => {
if (err instanceof Error) {
log(`Failed ${err.message}`);
this.setRelayState(relay, { status: "rejected", reason: err.message });
} else this.setRelayState(relay, { status: "rejected", reason: "Unknown" });
request.reject(err);
},
);
try {
// start signing request
const result = signer.signEvent(state.template);
if (result instanceof Promise) {
result.then(
(event) => promise.resolve(event),
(err) => promise.reject(err),
);
} else {
promise.resolve(result);
}
} catch (error) {
promise.reject(error);
}
}
/** cancel a pending authentication request */
cancel(relay: string) {
const state = this.getRelayState(relay);
if (!state) return;
const log = this.log.extend(relay);
log(`Canceling`);
// reject the promise if it exists
if (state.status === "requested" || state.status === "signing") state.promise.reject(new Error("Canceled"));
this.clearRelayState(relay);
}
getRelayState(relay: string): RelayAuthState | undefined {
return this.relayState$.value[relay];
}
protected setRelayState(relay: string, state: RelayAuthState) {
this.relayState$.next({ ...this.relayState$.value, [relay]: state });
}
protected clearRelayState(relay: string) {
if (!this.relayState$.value[relay]) return;
this.setRelayState(relay, { status: "dormant" });
}
protected getRelayAuthMode(relay: string): RelayAuthMode {
return this.relayMode.get(relay) || this.defaultMode;
}
/** handle relay state changes */
handleRelayConnectionState(packet: { from: string; state: ConnectionState }) {
const from = new URL(packet.from).toString();
// if the state is anything but connected, cancel any pending requests
if (packet.state !== "connected") this.cancel(from);
}
/** intercept sign requests and save them for later */
signEvent<K extends number>(draft: Nostr.EventParameters<K>): Promise<Nostr.Event<K>> {
if (!draft.tags) throw new Error("Missing tags");
let relay = draft.tags.find((t) => t[0] === "relay" && t[1])?.[1];
if (!relay) throw new Error("Missing relay tag");
// fix relay formatting
relay = new URL(relay).toString();
const log = this.log.extend(relay);
log(`Got request for ${relay}`);
const mode = this.getRelayAuthMode(relay);
// throw if mode is set to "never"
if (mode === "never") {
log(`Automatically rejecting`);
this.setRelayState(relay, { status: "rejected", reason: "Canceled" });
return Promise.reject(new Error("Authentication rejected"));
}
const challenge = draft.tags.find((t) => t[0] === "challenge" && t[1])?.[1];
if (!challenge) throw new Error("Missing challenge tag");
const promise = createDefer<NostrEvent>();
// add to pending
const template: EventTemplate = {
tags: [],
created_at: unixNow(),
...draft,
};
this.setRelayState(relay, { status: "requested", template, challenge, promise });
// start the authentication process imminently if set to "always"
if (mode === "always") {
log(`Automatically authenticating`);
this.authenticate(relay);
}
// @ts-expect-error
return promise;
}
async getPublicKey(): Promise<string> {
if (!this.signer) throw new Error("Missing signer");
return await this.signer.getPublicKey();
}
}
const authenticationSigner = new AuthenticationSigner(accounts.active$ as BehaviorSubject<Nip07Interface | undefined>);
// update signer based on local settings
localSettings.defaultAuthenticationMode.subscribe((mode) => (authenticationSigner.defaultMode = mode as RelayAuthMode));
localSettings.relayAuthenticationMode.subscribe((relays) => {
authenticationSigner.relayMode.clear();
for (const { relay, mode } of relays) {
authenticationSigner.relayMode.set(relay, mode);
}
});
export default authenticationSigner;

View File

@ -1,9 +1,8 @@
import _throttle from "lodash.throttle";
import { BehaviorSubject } from "rxjs";
import { createDefer, Deferred } from "applesauce-core/promise";
import createDefer, { Deferred } from "../classes/deferred";
import signingService from "./signing";
import accountService from "./accounts";
import { logger } from "../helpers/debug";
import accounts from "./accounts";

View File

@ -1,24 +0,0 @@
import { NostrEvent, kinds } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import ControlledObservable from "../classes/controlled-observable";
const deleteEventStream = new ControlledObservable<NostrEvent>();
function handleEvent(deleteEvent: NostrEvent) {
if (deleteEvent.kind !== kinds.EventDeletion) return;
deleteEventStream.next(deleteEvent);
}
function doesMatch(deleteEvent: NostrEvent, event: NostrEvent) {
const id = getEventUID(event);
return deleteEvent.tags.some((t) => (t[0] === "a" || t[0] === "e") && t[1] === id);
}
const deleteEventService = {
stream: deleteEventStream,
handleEvent,
doesMatch,
};
export default deleteEventService;

View File

@ -1,4 +1,7 @@
import { EventStore, QueryStore } from "applesauce-core";
import { isFromCache } from "applesauce-core/helpers";
import { cacheRelay$ } from "./cache-relay";
export const eventStore = new EventStore();
export const queryStore = new QueryStore(eventStore);
@ -9,3 +12,10 @@ if (import.meta.env.DEV) {
// @ts-expect-error debug
window.queryStore = queryStore;
}
// save all events to cache relay
eventStore.database.inserted.subscribe((event) => {
if (!isFromCache(event) && cacheRelay$.value) {
cacheRelay$.value.publish(event);
}
});

View File

@ -3,12 +3,14 @@ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { DEFAULT_SIGNAL_RELAYS } from "../const";
import {
ArrayLocalStorageEntry,
BooleanLocalStorageEntry,
NullableNumberLocalStorageEntry,
NumberLocalStorageEntry,
} from "../classes/local-settings/types";
import { LocalStorageEntry } from "../classes/local-settings/entry";
import { nanoid } from "nanoid";
import { RelayAuthMode } from "./authentication-signer";
// local relay
const idbMaxEvents = new NumberLocalStorageEntry("nostr-idb-max-events", 10_000);
@ -45,10 +47,13 @@ const verifyEventMethod = new LocalStorageEntry("verify-event-method", "wasm");
const enableKeyboardShortcuts = new BooleanLocalStorageEntry("enable-keyboard-shortcuts", true);
// privacy
const defaultAuthenticationMode = new LocalStorageEntry("default-relay-auth-mode", "ask"); // ask, always, never
const proactivelyAuthenticate = new BooleanLocalStorageEntry("proactively-authenticate", false);
const debugApi = new BooleanLocalStorageEntry("debug-api", false);
// relay authentication
const defaultAuthenticationMode = new LocalStorageEntry<RelayAuthMode>("default-authentication-mode", "ask");
const proactivelyAuthenticate = new BooleanLocalStorageEntry("proactively-authenticate", false);
const relayAuthenticationMode = new ArrayLocalStorageEntry<{relay: string,mode: RelayAuthMode}>("relay-authentication-mode", []);
// notifications
const deviceId = new LocalStorageEntry("device-id", nanoid());
@ -73,6 +78,7 @@ const localSettings = {
enableKeyboardShortcuts,
defaultAuthenticationMode,
proactivelyAuthenticate,
relayAuthenticationMode,
debugApi,
deviceId,
ntfyTopic,

View File

@ -1,10 +1,14 @@
import { createRxNostr } from "rx-nostr";
import { combineLatest } from "rxjs";
import { ConnectionState, createRxNostr } from "rx-nostr";
import { BehaviorSubject, combineLatest } from "rxjs";
import { nanoid } from "nanoid";
import verifyEvent from "./verify-event";
import { logger } from "../helpers/debug";
import clientRelaysService from "./client-relays";
import RelaySet from "../classes/relay-set";
import { unixNow } from "applesauce-core/helpers";
import authenticationSigner from "./authentication-signer";
const log = logger.extend("rx-nostr");
@ -15,6 +19,7 @@ const rxNostr = createRxNostr({
} catch (error) {}
return false;
},
authenticator: { signer: authenticationSigner },
connectionStrategy: "lazy-keep",
disconnectTimeout: 120_000,
});
@ -27,11 +32,31 @@ combineLatest([clientRelaysService.readRelays, clientRelaysService.writeRelays])
rxNostr.setDefaultRelays(relays.urls.map((url) => ({ url, read: read.has(url), write: write.has(url) })));
});
// keep track of all relay connection states
export const connections$ = new BehaviorSubject<Record<string, ConnectionState>>({});
rxNostr.createConnectionStateObservable().subscribe((packet) => {
// pass to authentication signer so it can cleanup
authenticationSigner.handleRelayConnectionState(packet);
const url = new URL(packet.from).toString();
connections$.next({ ...connections$.value, [url]: packet.state });
if (import.meta.env.DEV) log(packet.state, url);
});
// capture all notices sent from relays
export const notices$ = new BehaviorSubject<{ id: string; from: string; message: string; timestamp: number }[]>([]);
rxNostr.createAllMessageObservable().subscribe((packet) => {
if (packet.type === "NOTICE") {
const from = new URL(packet.from).toString();
const notice = { id: nanoid(), from, message: packet.notice, timestamp: unixNow() };
notices$.next([...notices$.value, notice]);
}
});
if (import.meta.env.DEV) {
// @ts-expect-error
window.rxNostr = rxNostr;
rxNostr.createConnectionStateObservable().subscribe((state) => log(state.state, state.from));
}
export default rxNostr;

View File

@ -31,7 +31,7 @@ import { getPubkeysFromList } from "../../../../helpers/nostr/lists";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserName from "../../../../components/user/user-name";
import UserDnsIdentity from "../../../../components/user/user-dns-identity";
import { RelayFavicon } from "../../../../components/relay-favicon";
import RelayFavicon from "../../../../components/relay-favicon";
export default function RelayStatusDetails({ event, ...props }: Omit<FlexProps, "children"> & { event: NostrEvent }) {
const selected = useContext(SelectedContext);

View File

@ -17,7 +17,7 @@ import { NostrEvent } from "nostr-tools";
import { getEventUID, getTagValue } from "../../../../helpers/nostr/event";
import SupportedNIPs from "../../../relays/components/supported-nips";
import { SelectedContext } from "../selected-context";
import { RelayFavicon } from "../../../../components/relay-favicon";
import RelayFavicon from "../../../../components/relay-favicon";
import Timestamp from "../../../../components/timestamp";
const IgnoreNips = [1, 2, 4, 11, 12, 15, 16];

View File

@ -10,7 +10,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { NostrEvent } from "../../types/nostr-event";
import { getListName, getRelaysFromList } from "../../helpers/nostr/lists";
import { RelayFavicon } from "../../components/relay-favicon";
import RelayFavicon from "../../components/relay-favicon";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
import { useReadRelays } from "../../hooks/use-client-relays";

View File

@ -23,7 +23,7 @@ import styled from "@emotion/styled";
import { Link as RouterLink } from "react-router-dom";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { RelayFavicon } from "../../../components/relay-favicon";
import RelayFavicon from "../../../components/relay-favicon";
import { CodeIcon } from "../../../components/icons";
import UserLink from "../../../components/user/user-link";
import UserAvatar from "../../../components/user/user-avatar";

View File

@ -4,7 +4,7 @@ import { Link as RouterLink } from "react-router-dom";
import { EventTemplate } from "nostr-tools";
import dayjs from "dayjs";
import { RelayFavicon } from "../../../components/relay-favicon";
import RelayFavicon from "../../../components/relay-favicon";
import useUserContactRelays from "../../../hooks/use-user-contact-relays";
import { CheckIcon } from "../../../components/icons";
import { useCallback, useState } from "react";

View File

@ -4,7 +4,7 @@ import { useActiveAccount } from "applesauce-react/hooks";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
import { Link as RouterLink } from "react-router-dom";
import { RelayFavicon } from "../../../components/relay-favicon";
import RelayFavicon from "../../../components/relay-favicon";
import SimpleView from "../../../components/layout/presets/simple-view";
function RelayItem({ url }: { url: string }) {

View File

@ -22,7 +22,7 @@ import RelayReviews from "./relay-reviews";
import RelayNotes from "./relay-notes";
import PeopleListProvider from "../../../providers/local/people-list-provider";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { RelayFavicon } from "../../../components/relay-favicon";
import RelayFavicon from "../../../components/relay-favicon";
import { safeRelayUrl } from "../../../helpers/relay";
import RelayUsersTab from "./relay-users";
const RelayDetailsTab = lazy(() => import("./relay-details"));

View File

@ -1,12 +1,12 @@
import { lazy } from "react";
import { RouteObject } from "react-router-dom";
import RelaysView from ".";
import AppRelaysView from "./app";
import CacheRelayView from "./cache";
import DatabaseView from "./cache/database";
import MailboxesView from "./mailboxes";
import SearchRelaysView from "./search";
import AppRelaysView from "../settings/relays";
import CacheRelayView from "../settings/cache";
import DatabaseView from "../settings/cache/database";
import MailboxesView from "../settings/mailboxes";
import MediaServersView from "../settings/media-servers";
import SearchRelaysView from "../settings/search";
import NIP05RelaysView from "./nip05";
import ContactListRelaysView from "./contact-list";
const WebRtcRelaysView = lazy(() => import("./webrtc"));

View File

@ -0,0 +1,30 @@
import { FormControl, FormLabel, Heading, SimpleGrid } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import DefaultAuthModeSelect from "../../../components/settings/default-auth-mode-select";
import SimpleView from "../../../components/layout/presets/simple-view";
import { connections$ } from "../../../services/rx-nostr";
import RelayAuthCard from "../../../components/relays/relay-auth-card";
export default function AuthenticationSettingsView() {
const connections = useObservable(connections$);
const sortedRelays = Object.keys(connections).sort();
return (
<SimpleView title="Authentication settings" maxW="6xl">
<FormControl>
<FormLabel htmlFor="default-mode">Default mode</FormLabel>
<DefaultAuthModeSelect id="default-mode" w="auto" />
</FormControl>
<Heading size="md" mt="4">
Relay mode
</Heading>
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }}>
{sortedRelays.map((relay) => (
<RelayAuthCard key={relay} relay={relay} />
))}
</SimpleGrid>
</SimpleView>
);
}

View File

@ -6,7 +6,7 @@ import { useObservable } from "applesauce-react/hooks";
import useAsyncErrorHandler from "../../../../hooks/use-async-error-handler";
import { controlApi$ } from "../../../../services/bakery";
import { RelayFavicon } from "../../../../components/relay-favicon";
import RelayFavicon from "../../../../components/relay-favicon";
function BroadcastRelay({ relay }: { relay: string }) {
const controlApi = useObservable(controlApi$);

View File

@ -4,7 +4,7 @@ import { CacheRelay, clearDB } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import { localDatabase, setCacheRelayURL } from "../../../../services/cache-relay";
import EnableWithDelete from "../components/enable-with-delete";
import EnableWithDelete from "./enable-with-delete";
import useCacheRelay from "../../../../hooks/use-cache-relay";
export default function InternalRelayCard() {

View File

@ -22,6 +22,7 @@ import Database01 from "../../components/icons/database-01";
import Mail02 from "../../components/icons/mail-02";
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
import useBakery from "../../hooks/use-bakery";
import CheckCircleBroken from "../../components/icons/check-circle-broken";
function DividerHeader({ title }: { title: string }) {
return (
@ -53,7 +54,7 @@ export default function SettingsView() {
Media Servers
</SimpleNavItem>
<SimpleNavItem to="/settings/search-relays" leftIcon={<SearchIcon boxSize={6} />}>
Search Relays
Search
</SimpleNavItem>
</>
)}
@ -65,6 +66,9 @@ export default function SettingsView() {
<SimpleNavItem to="/settings/relays" leftIcon={<RelayIcon boxSize={5} />}>
Relays
</SimpleNavItem>
<SimpleNavItem to="/settings/authentication" leftIcon={<CheckCircleBroken boxSize={5} />}>
Authentication
</SimpleNavItem>
<SimpleNavItem to="/settings/cache" leftIcon={<Database01 boxSize={5} />}>
Cache
</SimpleNavItem>

View File

@ -14,7 +14,7 @@ import { NostrEvent } from "../../../types/nostr-event";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { addRelayModeToMailbox, removeRelayModeFromMailbox } from "../../../helpers/nostr/mailbox";
import AddRelayForm from "../app/add-relay-form";
import AddRelayForm from "../relays/add-relay-form";
import DebugEventButton from "../../../components/debug-modal/debug-event-button";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { COMMON_CONTACT_RELAYS } from "../../../const";

View File

@ -1,5 +1,5 @@
import { MouseEventHandler, useCallback, useMemo } from "react";
import { Button, Card, CardBody, CardHeader, Flex, Heading, SimpleGrid, Switch, Text, Tooltip } from "@chakra-ui/react";
import { Button, Card, CardBody, CardHeader, Flex, Heading, SimpleGrid, Text } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { WarningIcon } from "@chakra-ui/icons";
import { useObservable } from "applesauce-react/hooks";
@ -21,9 +21,6 @@ import SelectRelaySet from "./select-relay-set";
import { safeRelayUrls } from "../../../helpers/relay";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import localSettings from "../../../services/local-settings";
import DefaultAuthModeSelect from "../../../components/settings/default-auth-mode-select";
import HelpCircle from "../../../components/icons/help-circle";
import SimpleView from "../../../components/layout/presets/simple-view";
const JapaneseRelays = safeRelayUrls([
@ -73,8 +70,6 @@ export default function AppRelaysView() {
const sorted = useMemo(() => RelaySet.from(readRelays, writeRelays).urls.sort(), [readRelays, writeRelays]);
const proactivelyAuthenticate = useObservable(localSettings.proactivelyAuthenticate);
return (
<SimpleView
title="App Relays"
@ -109,30 +104,6 @@ export default function AppRelaysView() {
</Text>
)}
<Heading size="md" mt="2">
Authentication
</Heading>
<Flex gap="2" alignItems="center">
<Text as="label" htmlFor="defaultAuthenticationMode">
Default:
</Text>
<DefaultAuthModeSelect size="sm" rounded="md" w="auto" />
<Switch
ms="4"
id="proactivelyAuthenticate"
isChecked={proactivelyAuthenticate}
onChange={(e) => localSettings.proactivelyAuthenticate.next(e.currentTarget.checked)}
/>
<Text as="label" htmlFor="proactivelyAuthenticate">
Proactively authenticate
</Text>
<Tooltip label="Authenticate to relays as soon as they send the authentication challenge">
<HelpCircle />
</Tooltip>
</Flex>
<Heading size="md" mt="2">
Set from
</Heading>

View File

@ -4,18 +4,14 @@ import { CloseIcon } from "@chakra-ui/icons";
import { useObservable } from "applesauce-react/hooks";
import { Flex, IconButton, Link } from "@chakra-ui/react";
import relayPoolService from "../../../services/relay-pool";
import clientRelaysService from "../../../services/client-relays";
import { RelayMode } from "../../../classes/relay";
import { RelayFavicon } from "../../../components/relay-favicon";
import RelayFavicon from "../../../components/relay-favicon";
import UploadCloud01 from "../../../components/icons/upload-cloud-01";
export default function RelayControl({ url }: { url: string }) {
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
const writeRelays = useObservable(clientRelaysService.writeRelays);
const color = relay.connected ? "green" : "red";
const onChange = () => {
if (writeRelays.has(url)) clientRelaysService.removeRelay(url, RelayMode.WRITE);
else clientRelaysService.addRelay(url, RelayMode.WRITE);
@ -23,7 +19,7 @@ export default function RelayControl({ url }: { url: string }) {
return (
<Flex gap="2" alignItems="center" pl="2">
<RelayFavicon relay={url} size="xs" outline="2px solid" outlineColor={color} />
<RelayFavicon relay={url} size="sm" />
<Link as={RouterLink} to={`/r/${encodeURIComponent(url)}`} isTruncated>
{url}
</Link>

View File

@ -5,15 +5,16 @@ import RequireActiveAccount from "../../components/router/require-active-account
import SettingsView from ".";
import DisplaySettings from "./display";
import AccountSettings from "./accounts";
import MailboxesView from "../relays/mailboxes";
import MailboxesView from "./mailboxes";
import MediaServersView from "./media-servers";
import SearchRelaysView from "../relays/search";
import AppRelaysView from "../relays/app";
import CacheRelayView from "../relays/cache";
import SearchRelaysView from "./search";
import AppRelaysView from "./relays";
import CacheRelayView from "./cache";
import PostSettings from "./post";
import PrivacySettings from "./privacy";
import LightningSettings from "./lightning";
import PerformanceSettings from "./performance";
import AuthenticationSettingsView from "./authentication";
// bakery settings
const BakeryConnectView = lazy(() => import("./bakery/connect"));
@ -40,6 +41,7 @@ export default [
),
},
{ path: "mailboxes", Component: MailboxesView },
{ path: "authentication", Component: AuthenticationSettingsView },
{ path: "media-servers", Component: MediaServersView },
{ path: "search-relays", Component: SearchRelaysView },
{ path: "relays", Component: AppRelaysView },

View File

@ -23,8 +23,8 @@ import useUserSearchRelayList from "../../../hooks/use-user-search-relay-list";
import { useActiveAccount } from "applesauce-react/hooks";
import { cloneList, getRelaysFromList, listAddRelay, listRemoveRelay } from "../../../helpers/nostr/lists";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import { RelayFavicon } from "../../../components/relay-favicon";
import AddRelayForm from "../app/add-relay-form";
import RelayFavicon from "../../../components/relay-favicon";
import AddRelayForm from "../relays/add-relay-form";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import SimpleView from "../../../components/layout/presets/simple-view";
@ -125,7 +125,7 @@ export default function SearchRelaysView() {
};
return (
<SimpleView title="Search Relays" maxW="4xl">
<SimpleView title="Search Settings" maxW="4xl">
<Text fontStyle="italic" px="2" mt="-2">
These relays are used to search for users and content
</Text>

View File

@ -2,7 +2,7 @@ import { Box, Button, Card, CardBody, Flex, Heading, SimpleGrid, Text } from "@c
import { useSet } from "react-use";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { RelayFavicon } from "../../../components/relay-favicon";
import RelayFavicon from "../../../components/relay-favicon";
import { containerProps } from "./common";
function RelayButton({ url, selected, onClick }: { url: string; selected: boolean; onClick: () => void }) {

View File

@ -1,5 +0,0 @@
import DatabaseView from "../../relays/cache/database";
export default function TaskManagerDatabase() {
return <DatabaseView />;
}

View File

@ -1,7 +1,7 @@
import { Tab, TabIndicator, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
const tabs = ["publish-log", "relays", "processes", "database"];
const tabs = ["publish-log", "relays"];
export default function TaskManagerLayout() {
const location = useLocation();
@ -27,8 +27,6 @@ export default function TaskManagerLayout() {
<TabList overflowX="auto" overflowY="hidden" flexShrink={0} mr="10">
<Tab>Publish Log</Tab>
<Tab>Relays</Tab>
<Tab>Processes</Tab>
<Tab>Database</Tab>
</TabList>
<TabIndicator height="2px" bg="primary.500" borderRadius="1px" />
@ -39,12 +37,6 @@ export default function TaskManagerLayout() {
<TabPanel p={0} minH="50vh">
<Outlet />
</TabPanel>
<TabPanel p={0} minH="50vh">
<Outlet />
</TabPanel>
<TabPanel minH="50vh">
<Outlet />
</TabPanel>
</TabPanels>
</Tabs>
);

View File

@ -32,30 +32,6 @@ export default function TaskManagerModal({
>
<RouterProvider router={router} />
</Suspense>
{/* <Tabs
display="flex"
flexDirection="column"
flexGrow="1"
isLazy
colorScheme="primary"
position="relative"
variant="unstyled"
>
<TabList overflowX="auto" overflowY="hidden" flexShrink={0} mr="10">
<Tab>Network</Tab>
<Tab>Database</Tab>
</TabList>
<TabIndicator height="2px" bg="primary.500" borderRadius="1px" />
<TabPanels minH="50vh">
<TabPanel p={0}>
<TaskManagerNetwork />
</TabPanel>
<TabPanel>
<DatabaseView />
</TabPanel>
</TabPanels>
</Tabs> */}
</ModalBody>
<ModalCloseButton />
</ModalContent>

View File

@ -6,9 +6,7 @@ import InspectRelayView from "./relays/inspect-relay";
import TaskManagerModal from "./modal";
import TaskManagerLayout from "./layout";
import TaskManagerRelays from "./relays";
import TaskManagerDatabase from "./database";
import PublishLogView from "./publish-log";
import TaskManagerProcesses from "./processes";
import useRouterMarker from "../../hooks/use-router-marker";
type Router = ReturnType<typeof createMemoryRouter>;
@ -31,21 +29,12 @@ const routes: RouteObject[] = [
children: [
{
path: "relays",
element: <TaskManagerRelays />,
},
{
path: "r/:url",
element: <InspectRelayView />,
},
{
path: "processes",
element: <TaskManagerProcesses />,
children: [
{ index: true, Component: TaskManagerRelays },
{ path: ":relay", Component: InspectRelayView },
],
},
{ path: "publish-log", element: <PublishLogView /> },
{
path: "database",
element: <TaskManagerDatabase />,
},
],
},
];

View File

@ -4,37 +4,44 @@ import { useObservable } from "applesauce-react/hooks";
import { CheckIcon, ErrorIcon } from "../../../components/icons";
import { PublishLogEntry } from "../../../providers/global/publish-provider";
export function usePublishLogEntryStatus(entry: PublishLogEntry) {
const { relays } = useObservable(entry);
const total = entry.relays.length;
const successful = Object.values(relays).filter((p) => p.ok);
const failedWithNotice = Object.values(relays).filter((p) => !p.ok && !!p.notice);
let icon = <Spinner size="xs" />;
let color: TagProps["colorScheme"] = "blue";
if (Object.keys(relays).length !== entry.relays.length) {
color = "blue";
icon = <Spinner size="xs" />;
} else if (successful.length === 0) {
color = "red";
icon = <ErrorIcon />;
} else if (failedWithNotice.length > 0) {
color = "orange";
icon = <CheckIcon />;
} else {
color = "green";
icon = <CheckIcon />;
}
return { color, icon, successful, failedWithNotice, total };
}
export default function PublishActionStatusTag({
entry,
...props
}: { entry: PublishLogEntry } & Omit<TagProps, "children">) {
const { relays } = useObservable(entry);
const successful = Object.values(relays).filter((p) => p.ok);
const failedWithNotice = Object.values(relays).filter((p) => !p.ok && !!p.notice);
let statusIcon = <Spinner size="xs" />;
let statusColor: TagProps["colorScheme"] = "blue";
if (Object.keys(relays).length !== entry.relays.length) {
statusColor = "blue";
statusIcon = <Spinner size="xs" />;
} else if (successful.length === 0) {
statusColor = "red";
statusIcon = <ErrorIcon />;
} else if (failedWithNotice.length > 0) {
statusColor = "orange";
statusIcon = <CheckIcon />;
} else {
statusColor = "green";
statusIcon = <CheckIcon />;
}
const { icon, color, successful, total } = usePublishLogEntryStatus(entry);
return (
<Tag colorScheme={statusColor} {...props}>
<Tag colorScheme={color} {...props}>
<TagLabel mr="1">
{successful.length}/{entry.relays.length}
{successful.length}/{total}
</TagLabel>
{statusIcon}
{icon}
</Tag>
);
}

View File

@ -1,179 +1,45 @@
import { useMemo } from "react";
import {
Badge,
Box,
Flex,
Link,
LinkBox,
Select,
SimpleGrid,
Spacer,
Switch,
Tab,
TabIndicator,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useInterval,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { useLocalStorage } from "react-use";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { Tab, TabIndicator, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { combineLatest, map } from "rxjs";
import relayPoolService from "../../../services/relay-pool";
import { RelayFavicon } from "../../../components/relay-favicon";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import { IconRelayAuthButton, useRelayAuthMethod } from "../../../components/relays/relay-auth-button";
import RelayConnectSwitch from "../../../components/relays/relay-connect-switch";
import useRouteSearchValue from "../../../hooks/use-route-search-value";
import processManager from "../../../services/process-manager";
import { RelayAuthMode } from "../../../classes/relay-pool";
import Timestamp from "../../../components/timestamp";
import localSettings from "../../../services/local-settings";
import useForceUpdate from "../../../hooks/use-force-update";
import DefaultAuthModeSelect from "../../../components/settings/default-auth-mode-select";
import useCacheRelay from "../../../hooks/use-cache-relay";
function RelayCard({ relay }: { relay: AbstractRelay }) {
return (
<Flex gap="2" p="2" alignItems="center" borderWidth={1} rounded="md">
<RelayFavicon relay={relay.url} size="sm" mr="2" />
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} isTruncated fontWeight="bold" py="1" pr="10">
{relay.url}
</Link>
<Spacer />
<IconRelayAuthButton relay={relay} size="sm" variant="ghost" />
<RelayConnectSwitch relay={relay} />
</Flex>
);
}
function RelayAuthCard({ relay }: { relay: AbstractRelay }) {
const { authenticated } = useRelayAuthMethod(relay);
const defaultMode = useObservable(localSettings.defaultAuthenticationMode);
const processes = processManager.getRootProcessesForRelay(relay);
const [authMode, setAuthMode] = useLocalStorage<RelayAuthMode | "">(
relayPoolService.getRelayAuthStorageKey(relay),
"",
{
raw: true,
},
);
return (
<Flex gap="2" p="2" alignItems="center" borderWidth={1} rounded="md">
<RelayFavicon relay={relay.url} size="sm" mr="2" />
<Box isTruncated>
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} fontWeight="bold">
{relay.url}
</Link>
<br />
{authenticated ? <Badge colorScheme="green">Authenticated</Badge> : <Text>{processes.size} Processes</Text>}
</Box>
<Spacer />
<Select
size="sm"
w="auto"
rounded="md"
flexShrink={0}
value={authMode}
onChange={(e) => setAuthMode(e.target.value as RelayAuthMode)}
>
<option value="">Default ({defaultMode})</option>
<option value="always">Always</option>
<option value="ask">Ask</option>
<option value="never">Never</option>
</Select>
<IconRelayAuthButton relay={relay} variant="ghost" flexShrink={0} />
</Flex>
);
}
import { connections$, notices$ } from "../../../services/rx-nostr";
import RelayConnectionsTab from "./tabs/connections";
import RelayAuthenticationTab from "./tabs/authentication";
import NoticesTab from "./tabs/notices";
import authenticationSigner from "../../../services/authentication-signer";
const TABS = ["relays", "auth", "notices"];
export default function TaskManagerRelays() {
const update = useForceUpdate();
useInterval(update, 2000);
const cacheRelay = useCacheRelay();
const { value: tab, setValue: setTab } = useRouteSearchValue("tab", TABS[0]);
const tabIndex = TABS.indexOf(tab);
const relays = Array.from(relayPoolService.relays.values())
.filter((r) => r !== cacheRelay)
.sort((a, b) => +b.connected - +a.connected || a.url.localeCompare(b.url));
const notices = useObservable(notices$);
const observable = useMemo(
() =>
combineLatest(Array.from(relayPoolService.notices.values())).pipe(
map((relays) => relays.flat().sort((a, b) => b.date - a.date)),
),
[],
);
const notices = useObservable(observable) ?? [];
const challenges = Array.from(relayPoolService.challenges.entries()).filter(([r, c]) => r.connected && !!c.value);
const proactivelyAuthenticate = useObservable(localSettings.proactivelyAuthenticate);
const connections = useObservable(connections$);
const connected = Object.values(connections).reduce((t, s) => (s === "connected" ? t + 1 : t), 0);
const pending = useObservable(authenticationSigner.relayState$);
return (
<Tabs position="relative" variant="unstyled" index={tabIndex} onChange={(i) => setTab(TABS[i])} isLazy>
<TabList>
<Tab>Relays ({relays.length})</Tab>
<Tab>Authentication ({challenges.length})</Tab>
<Tab>
Relays ({connected}/{Object.keys(connections).length})
</Tab>
<Tab>Authentication ({Object.keys(pending).length})</Tab>
<Tab>Notices ({notices.length})</Tab>
</TabList>
<TabIndicator mt="-1.5px" height="2px" bg="primary.500" borderRadius="1px" />
<TabPanels>
<TabPanel p="0">
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{cacheRelay instanceof AbstractRelay && <RelayCard relay={cacheRelay} />}
{relays.map((relay) => (
<RelayCard key={relay.url} relay={relay} />
))}
</SimpleGrid>
<RelayConnectionsTab />
</TabPanel>
<TabPanel p="0">
<Flex gap="2" px="2" pt="2" alignItems="center">
<Text as="label" htmlFor="defaultAuthenticationMode">
Default:
</Text>
<DefaultAuthModeSelect id="defaultAuthenticationMode" w="auto" size="sm" rounded="md" />
<Switch
ms="4"
id="proactivelyAuthenticate"
isChecked={proactivelyAuthenticate}
onChange={(e) => localSettings.proactivelyAuthenticate.next(e.currentTarget.checked)}
/>
<Text as="label" htmlFor="proactivelyAuthenticate">
Proactively authenticate
</Text>
</Flex>
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{challenges.map(([relay, challenge]) => (
<RelayAuthCard key={relay.url} relay={relay} />
))}
</SimpleGrid>
<RelayAuthenticationTab />
</TabPanel>
<TabPanel p="0">
{notices.map((notice) => (
<LinkBox key={notice.date + notice.message} px="2" py="1" fontFamily="monospace">
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(notice.relay.url)}`} fontWeight="bold">
{notice.relay.url}
</HoverLinkOverlay>
<Timestamp timestamp={notice.date} ml={2} />
<Text fontFamily="monospace">{notice.message}</Text>
</LinkBox>
))}
<NoticesTab />
</TabPanel>
</TabPanels>
</Tabs>

View File

@ -1,64 +1,50 @@
import { useMemo } from "react";
import {
Flex,
Heading,
Spacer,
Tab,
TabIndicator,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useInterval,
} from "@chakra-ui/react";
import { useParams } from "react-router-dom";
import { useObservable } from "applesauce-react/hooks";
import { Flex, Heading, Spacer, Tab, TabIndicator, TabList, TabPanel, TabPanels, Tabs, Text } from "@chakra-ui/react";
import { Navigate, useParams } from "react-router-dom";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import BackButton from "../../../components/router/back-button";
import relayPoolService from "../../../services/relay-pool";
import ProcessBranch from "../processes/process/process-tree";
import processManager from "../../../services/process-manager";
import { IconRelayAuthButton } from "../../../components/relays/relay-auth-button";
import { RelayStatus } from "../../../components/relays/relay-status";
import Timestamp from "../../../components/timestamp";
import { RelayAuthIconButton } from "../../../components/relays/relay-auth-icon-button";
import RelayStatusBadge from "../../../components/relays/relay-status";
import RelayConnectSwitch from "../../../components/relays/relay-connect-switch";
import useForceUpdate from "../../../hooks/use-force-update";
import useRelayNotices from "../../../hooks/use-relay-notices";
import Timestamp from "../../../components/timestamp";
export default function InspectRelayView() {
const { url } = useParams();
if (!url) throw new Error("Missing url param");
const update = useForceUpdate();
useInterval(update, 500);
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
const { relay } = useParams();
if (!relay) return <Navigate to="/" />;
const rootProcesses = processManager.getRootProcessesForRelay(relay);
const notices = useObservable(relayPoolService.notices.get(relay));
const notices = useRelayNotices(relay);
return (
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<BackButton size="sm" />
<Heading size="md">{url}</Heading>
<RelayStatus relay={relay} />
<Heading size="md">{relay}</Heading>
<RelayStatusBadge relay={relay} />
<Spacer />
<IconRelayAuthButton relay={relay} size="sm" variant="ghost" />
<RelayAuthIconButton relay={relay} size="sm" variant="ghost" />
<RelayConnectSwitch relay={relay} />
</Flex>
<Tabs position="relative" variant="unstyled">
<TabList>
<Tab>Processes ({rootProcesses.size})</Tab>
<Tab>Notices ({notices.length})</Tab>
{/* <Tab>Processes ({rootProcesses.size})</Tab> */}
</TabList>
<TabIndicator mt="-1.5px" height="2px" bg="primary.500" borderRadius="1px" />
<TabPanels>
<TabPanel p="0">
{notices.map((notice, i) => (
<Text fontFamily="monospace" key={notice.id}>
{notice.message} <Timestamp timestamp={notice.timestamp} />
</Text>
))}
</TabPanel>
{/* <TabPanel p="0">
{Array.from(rootProcesses).map((process) => (
<ProcessBranch
key={process.id}
@ -66,14 +52,7 @@ export default function InspectRelayView() {
filter={(p) => (p.relays.size > 0 ? p.relays.has(relay) : p.children.size > 0)}
/>
))}
</TabPanel>
<TabPanel p="0">
{notices.map((notice, i) => (
<Text fontFamily="monospace" key={notice.date + i}>
{notice.message} <Timestamp timestamp={notice.date} color="gray.500" />
</Text>
))}
</TabPanel>
</TabPanel> */}
</TabPanels>
</Tabs>
</VerticalPageLayout>

View File

@ -0,0 +1,26 @@
import { Flex, SimpleGrid, Text } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import DefaultAuthModeSelect from "../../../../components/settings/default-auth-mode-select";
import authenticationSigner from "../../../../services/authentication-signer";
import RelayAuthCard from "../../../../components/relays/relay-auth-card";
export default function RelayAuthenticationTab() {
const relayState = useObservable(authenticationSigner.relayState$);
return (
<>
<Flex gap="2" px="2" pt="2" alignItems="center">
<Text as="label" htmlFor="defaultAuthenticationMode">
Default mode:
</Text>
<DefaultAuthModeSelect id="defaultAuthenticationMode" w="auto" />
</Flex>
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{Object.keys(relayState).map((relay) => (
<RelayAuthCard key={relay} relay={relay} />
))}
</SimpleGrid>
</>
);
}

View File

@ -0,0 +1,36 @@
import { Flex, Link, SimpleGrid, Spacer } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { connections$ } from "../../../../services/rx-nostr";
import RelayFavicon from "../../../../components/relay-favicon";
import RouterLink from "../../../../components/router-link";
import { RelayAuthIconButton } from "../../../../components/relays/relay-auth-icon-button";
import RelayStatusBadge from "../../../../components/relays/relay-status";
function RelayCard({ relay }: { relay: string }) {
return (
<Flex gap="2" p="2" alignItems="center" borderWidth={1} rounded="md">
<RelayFavicon relay={relay} size="sm" showStatus />
<Link as={RouterLink} to={`/relays/${encodeURIComponent(relay)}`} isTruncated fontWeight="bold" py="1" pr="5">
{relay}
</Link>
<Spacer />
<RelayAuthIconButton relay={relay} size="sm" variant="ghost" />
<RelayStatusBadge relay={relay} />
</Flex>
);
}
export default function RelayConnectionsTab() {
const connections = useObservable(connections$);
return (
<Flex direction="column">
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{Object.entries(connections).map(([relay]) => (
<RelayCard key={relay} relay={relay} />
))}
</SimpleGrid>
</Flex>
);
}

View File

@ -0,0 +1,25 @@
import { LinkBox, Text } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import HoverLinkOverlay from "../../../../components/hover-link-overlay";
import RouterLink from "../../../../components/router-link";
import Timestamp from "../../../../components/timestamp";
import { notices$ } from "../../../../services/rx-nostr";
export default function NoticesTab() {
const notices = useObservable(notices$);
return (
<>
{notices.map((notice) => (
<LinkBox key={notice.timestamp + notice.message} px="2" py="1" fontFamily="monospace">
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(notice.from)}`} fontWeight="bold">
{notice.from}
</HoverLinkOverlay>
<Timestamp timestamp={notice.timestamp} ml={2} />
<Text fontFamily="monospace">{notice.message}</Text>
</LinkBox>
))}
</>
);
}

View File

@ -5,7 +5,7 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import { NostrEvent } from "../../types/nostr-event";
import RelayReviewNote from "../relays/components/relay-review-note";
import { RelayFavicon } from "../../components/relay-favicon";
import RelayFavicon from "../../components/relay-favicon";
import { RelayDebugButton, RelayMetadata } from "../relays/components/relay-card";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";