mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
Update task manager to reflect rx-nostr relay connections and auth
This commit is contained in:
parent
41e02e14a5
commit
3d7a5bd13a
5
.changeset/giant-pumas-joke.md
Normal file
5
.changeset/giant-pumas-joke.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Update task manager to reflect rx-nostr relay connections and auth
|
@ -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
142
pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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) {
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
18
src/components/layout/components/connections-button.tsx
Normal file
18
src/components/layout/components/connections-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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} /> */}
|
||||
</>
|
||||
);
|
||||
}
|
28
src/components/layout/components/publish-log-button.tsx
Normal file
28
src/components/layout/components/publish-log-button.tsx
Normal 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} />;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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">;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
43
src/components/relays/relay-auth-card.tsx
Normal file
43
src/components/relays/relay-auth-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
52
src/components/relays/relay-auth-icon-button.tsx
Normal file
52
src/components/relays/relay-auth-icon-button.tsx
Normal 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;
|
||||
}
|
||||
}
|
36
src/components/relays/relay-auth-mode-select.tsx
Normal file
36
src/components/relays/relay-auth-mode-select.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
7
src/hooks/use-relay-auth-state.ts
Normal file
7
src/hooks/use-relay-auth-state.ts
Normal 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];
|
||||
}
|
7
src/hooks/use-relay-connection-state.ts
Normal file
7
src/hooks/use-relay-connection-state.ts
Normal 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];
|
||||
}
|
6
src/hooks/use-relay-notices.ts
Normal file
6
src/hooks/use-relay-notices.ts
Normal 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);
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
200
src/services/authentication-signer.ts
Normal file
200
src/services/authentication-signer.ts
Normal 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;
|
@ -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";
|
||||
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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];
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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 }) {
|
||||
|
@ -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"));
|
||||
|
@ -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"));
|
||||
|
30
src/views/settings/authentication/index.tsx
Normal file
30
src/views/settings/authentication/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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$);
|
||||
|
@ -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() {
|
@ -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>
|
||||
|
@ -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";
|
@ -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>
|
@ -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>
|
@ -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 },
|
||||
|
@ -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>
|
@ -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 }) {
|
||||
|
@ -1,5 +0,0 @@
|
||||
import DatabaseView from "../../relays/cache/database";
|
||||
|
||||
export default function TaskManagerDatabase() {
|
||||
return <DatabaseView />;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
26
src/views/task-manager/relays/tabs/authentication.tsx
Normal file
26
src/views/task-manager/relays/tabs/authentication.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
36
src/views/task-manager/relays/tabs/connections.tsx
Normal file
36
src/views/task-manager/relays/tabs/connections.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
src/views/task-manager/relays/tabs/notices.tsx
Normal file
25
src/views/task-manager/relays/tabs/notices.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user