Update timelines to use applesauce

This commit is contained in:
hzrd149 2024-10-10 17:14:40 +01:00
parent 0e2054453e
commit 60b61e96b8
114 changed files with 5516 additions and 4622 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Update timelines to use applesauce

View File

@ -1,7 +1,7 @@
identifier: noStrudel
maintainers:
- npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr
- npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr
relays:
- wss://nostrue.com/
- wss://nostr.wine/
- wss://nos.lol/
- wss://nostrue.com/
- wss://nostr.wine/
- wss://nos.lol/

8631
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

168
server/pnpm-lock.yaml generated
View File

@ -1,11 +1,10 @@
lockfileVersion: '9.0'
lockfileVersion: "9.0"
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
cors-anywhere:
@ -16,147 +15,180 @@ importers:
version: 7.0.2
packages:
'@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
"@tootallnate/quickjs-emscripten@0.23.0":
resolution:
{ integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== }
agent-base@7.1.1:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== }
engines: { node: ">= 14" }
ast-types@0.13.4:
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
engines: {node: '>=4'}
resolution:
{ integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== }
engines: { node: ">=4" }
basic-ftp@5.0.5:
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
engines: {node: '>=10.0.0'}
resolution:
{ integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== }
engines: { node: ">=10.0.0" }
cors-anywhere@0.4.4:
resolution: {integrity: sha512-8OBFwnzMgR4mNrAeAyOLB2EruS2z7u02of2bOu7i9kKYlZG+niS7CTHLPgEXKWW2NAOJWRry9RRCaL9lJRjNqg==}
engines: {node: '>=0.10.0'}
resolution:
{ integrity: sha512-8OBFwnzMgR4mNrAeAyOLB2EruS2z7u02of2bOu7i9kKYlZG+niS7CTHLPgEXKWW2NAOJWRry9RRCaL9lJRjNqg== }
engines: { node: ">=0.10.0" }
data-uri-to-buffer@6.0.2:
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== }
engines: { node: ">= 14" }
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
resolution:
{ integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== }
engines: { node: ">=6.0" }
peerDependencies:
supports-color: '*'
supports-color: "*"
peerDependenciesMeta:
supports-color:
optional: true
degenerator@5.0.1:
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== }
engines: { node: ">= 14" }
escodegen@2.1.0:
resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
engines: {node: '>=6.0'}
resolution:
{ integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== }
engines: { node: ">=6.0" }
hasBin: true
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
resolution:
{ integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== }
engines: { node: ">=4" }
hasBin: true
estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
resolution:
{ integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== }
engines: { node: ">=4.0" }
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
resolution:
{ integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== }
engines: { node: ">=0.10.0" }
eventemitter3@1.2.0:
resolution: {integrity: sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA==}
resolution:
{ integrity: sha512-DOFqA1MF46fmZl2xtzXR3MPCRsXqgoFqdXcrCVYM3JNnfUeHTm/fh/v/iU7gBFpwkuBmoJPAm5GuhdDfSEJMJA== }
fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'}
resolution:
{ integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== }
engines: { node: ">=14.14" }
get-uri@6.0.3:
resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== }
engines: { node: ">= 14" }
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
resolution:
{ integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== }
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== }
engines: { node: ">= 14" }
http-proxy@1.11.1:
resolution: {integrity: sha512-qz7jZarkVG3G6GMq+4VRJPSN4NkIjL4VMTNhKGd8jc25BumeJjWWvnY3A7OkCGa8W1TTxbaK3dcE0ijFalITVA==}
engines: {node: '>=0.10.0'}
resolution:
{ integrity: sha512-qz7jZarkVG3G6GMq+4VRJPSN4NkIjL4VMTNhKGd8jc25BumeJjWWvnY3A7OkCGa8W1TTxbaK3dcE0ijFalITVA== }
engines: { node: ">=0.10.0" }
https-proxy-agent@7.0.5:
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== }
engines: { node: ">= 14" }
ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'}
resolution:
{ integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== }
engines: { node: ">= 12" }
jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
resolution:
{ integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== }
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
resolution:
{ integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== }
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
resolution:
{ integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== }
netmask@2.0.2:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
resolution:
{ integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== }
engines: { node: ">= 0.4.0" }
pac-proxy-agent@7.0.2:
resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== }
engines: { node: ">= 14" }
pac-resolver@7.0.1:
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== }
engines: { node: ">= 14" }
proxy-from-env@0.0.1:
resolution: {integrity: sha512-B9Hnta3CATuMS0q6kt5hEezOPM+V3dgaRewkFtFoaRQYTVNsHqUvFXmndH06z3QO1ZdDnRELv5vfY6zAj/gG7A==}
resolution:
{ integrity: sha512-B9Hnta3CATuMS0q6kt5hEezOPM+V3dgaRewkFtFoaRQYTVNsHqUvFXmndH06z3QO1ZdDnRELv5vfY6zAj/gG7A== }
requires-port@0.0.1:
resolution: {integrity: sha512-AzPDCliPoWDSvEVYRQmpzuPhGGEnPrQz9YiOEvn+UdB9ixBpw+4IOZWtwctmpzySLZTy7ynpn47V14H4yaowtA==}
resolution:
{ integrity: sha512-AzPDCliPoWDSvEVYRQmpzuPhGGEnPrQz9YiOEvn+UdB9ixBpw+4IOZWtwctmpzySLZTy7ynpn47V14H4yaowtA== }
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
resolution:
{ integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== }
engines: { node: ">= 6.0.0", npm: ">= 3.0.0" }
socks-proxy-agent@8.0.4:
resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==}
engines: {node: '>= 14'}
resolution:
{ integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== }
engines: { node: ">= 14" }
socks@2.8.3:
resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
resolution:
{ integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== }
engines: { node: ">= 10.0.0", npm: ">= 3.0.0" }
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
resolution:
{ integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== }
engines: { node: ">=0.10.0" }
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
resolution:
{ integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== }
tslib@2.7.0:
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
resolution:
{ integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== }
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
resolution:
{ integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== }
engines: { node: ">= 10.0.0" }
snapshots:
'@tootallnate/quickjs-emscripten@0.23.0': {}
"@tootallnate/quickjs-emscripten@0.23.0": {}
agent-base@7.1.1:
dependencies:
@ -258,7 +290,7 @@ snapshots:
pac-proxy-agent@7.0.2:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
"@tootallnate/quickjs-emscripten": 0.23.0
agent-base: 7.1.1
debug: 4.3.7
get-uri: 6.0.3

View File

@ -1,21 +1,20 @@
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import { EventStore } from "applesauce-core";
import debug, { Debugger } from "debug";
import EventStore from "./event-store";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
import BracketsX from "../components/icons/brackets-x";
import processManager from "../services/process-manager";
import createDefer, { Deferred } from "./deferred";
import { eventStore } from "../services/event-store";
/** This class is used to batch requests for single events from a relay */
export default class BatchEventLoader {
events = new EventStore();
relay: AbstractRelay;
process: Process;
store: EventStore;
subscription: PersistentSubscription;
@ -27,8 +26,9 @@ export default class BatchEventLoader {
log: Debugger;
constructor(relay: AbstractRelay, log?: Debugger) {
constructor(store: EventStore, relay: AbstractRelay, log?: Debugger) {
this.relay = relay;
this.store = store;
this.log = log || debug("BatchEventLoader");
this.process = new Process("BatchEventLoader", this, [relay]);
this.process.icon = BracketsX;
@ -41,15 +41,15 @@ export default class BatchEventLoader {
this.process.addChild(this.subscription.process);
}
requestEvent(id: string): Promise<NostrEvent | null> {
const event = this.events.getEvent(id);
requestEvent(uid: string): Promise<NostrEvent | null> {
const event = this.store.getEvent(uid);
if (!event) {
if (this.pending.has(id)) return this.pending.get(id)!;
if (this.next.has(id)) return this.next.get(id)!;
if (this.pending.has(uid)) return this.pending.get(uid)!;
if (this.next.has(uid)) return this.next.get(uid)!;
const defer = createDefer<NostrEvent | null>();
this.next.set(id, defer);
this.next.set(uid, defer);
// request subscription update
this.start();
@ -73,11 +73,10 @@ export default class BatchEventLoader {
);
private handleEvent(event: NostrEvent) {
event = eventStore.add(event, this.relay.url);
event = this.store.add(event, this.relay.url);
const key = event.id;
this.events.addEvent(event);
this.pending.get(key)?.resolve(event);
this.pending.delete(key);
}

View File

@ -2,7 +2,8 @@ import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import { getEventUID } from "nostr-idb";
import { EventStore } from "applesauce-core";
import { getEventUID } from "applesauce-core/helpers";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
@ -11,10 +12,10 @@ import createDefer, { Deferred } from "./deferred";
import Dataflow04 from "../components/icons/dataflow-04";
import SuperMap from "./super-map";
import Subject from "./subject";
import { eventStore } from "../services/event-store";
/** Batches requests for events with #d tags from a single relay */
export default class BatchIdentifierLoader {
store: EventStore;
kinds: number[];
relay: AbstractRelay;
process: Process;
@ -36,7 +37,8 @@ export default class BatchIdentifierLoader {
log: Debugger;
constructor(relay: AbstractRelay, kinds: number[], log?: Debugger) {
constructor(store: EventStore, relay: AbstractRelay, kinds: number[], log?: Debugger) {
this.store = store;
this.relay = relay;
this.kinds = kinds;
this.log = log || debug("BatchIdentifierLoader");
@ -82,7 +84,7 @@ export default class BatchIdentifierLoader {
);
handleEvent(event: NostrEvent) {
event = eventStore.add(event, this.relay.url);
event = this.store.add(event, this.relay.url);
// add event to cache
for (const tag of event.tags) {

View File

@ -2,15 +2,14 @@ import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import { EventStore } from "applesauce-core";
import EventStore from "./event-store";
import { getEventUID } from "../helpers/nostr/event";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
import BracketsX from "../components/icons/brackets-x";
import processManager from "../services/process-manager";
import createDefer, { Deferred } from "./deferred";
import { eventStore } from "../services/event-store";
export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
@ -18,7 +17,7 @@ export function createCoordinate(kind: number, pubkey: string, d?: string) {
/** This class is used to batch requests by kind and pubkey to a single relay */
export default class BatchKindPubkeyLoader {
events = new EventStore();
store: EventStore;
relay: AbstractRelay;
process: Process;
@ -32,7 +31,8 @@ export default class BatchKindPubkeyLoader {
log: Debugger;
constructor(relay: AbstractRelay, log?: Debugger) {
constructor(store: EventStore, relay: AbstractRelay, log?: Debugger) {
this.store = store;
this.relay = relay;
this.log = log || debug("BatchKindPubkeyLoader");
this.process = new Process("BatchKindPubkeyLoader", this, [relay]);
@ -48,7 +48,7 @@ export default class BatchKindPubkeyLoader {
requestEvent(kind: number, pubkey: string, d?: string): Promise<NostrEvent | null> {
const key = createCoordinate(kind, pubkey, d);
const event = this.events.getEvent(key);
const event = this.store.getEvent(key);
if (!event) {
if (this.pending.has(key)) return this.pending.get(key)!;
@ -79,16 +79,16 @@ export default class BatchKindPubkeyLoader {
);
private handleEvent(event: NostrEvent) {
event = eventStore.add(event, this.relay.url);
event = this.store.add(event, this.relay.url);
const key = getEventUID(event);
const defer = this.pending.get(key);
if (defer) this.pending.delete(key);
const current = this.events.getEvent(key);
const current = this.store.getEvent(key);
if (!current || event.created_at > current.created_at) {
this.events.addEvent(event);
this.store.add(event);
if (defer) defer.resolve(event);
} else if (defer) defer.resolve(null);

View File

@ -19,7 +19,7 @@ import { eventStore } from "../services/event-store";
const DEFAULT_CHUNK_SIZE = 100;
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
export type EventFilter = (event: NostrEvent) => boolean;
export default class ChunkedRequest {
id: string;

View File

@ -6,7 +6,7 @@ import ControlledObservable from "./controlled-observable";
import SuperMap from "./super-map";
import deleteEventService from "../services/delete-events";
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
export type EventFilter = (event: NostrEvent) => boolean;
/** a class used to store and sort events */
export default class EventStore {
@ -96,7 +96,7 @@ export default class EventStore {
while (true) {
const event = events.shift();
if (!event) return;
if (filter && !filter(event, this)) continue;
if (filter && !filter(event)) continue;
if (i === nth) return event;
i++;
}
@ -108,7 +108,7 @@ export default class EventStore {
while (true) {
const event = events.pop();
if (!event) return;
if (filter && !filter(event, this)) continue;
if (filter && !filter(event)) continue;
if (i === nth) return event;
i++;
}

View File

@ -1,8 +1,8 @@
import { nanoid } from "nanoid";
import { SimpleRelay, Subscription, SubscriptionOptions } from "nostr-idb";
import { Filter, NostrEvent, matchFilters } from "nostr-tools";
import { EventStore } from "applesauce-core";
import EventStore from "./event-store";
import { logger } from "../helpers/debug";
export default class MemoryRelay implements SimpleRelay {
@ -10,62 +10,39 @@ export default class MemoryRelay implements SimpleRelay {
connected = true;
url = ":memory:";
events = new EventStore();
store = new EventStore();
subscriptions = new Map<string, Subscription>();
constructor() {
this.events.onEvent.subscribe((event) => {
for (const [id, sub] of this.subscriptions) {
if (sub.onevent && matchFilters(sub.filters, event)) sub.onevent(event);
}
});
}
async connect() {}
close(): void {}
async publish(event: NostrEvent) {
this.events.addEvent(event);
return "accepted";
}
private async executeSubscription(sub: Subscription) {
const limit = sub.filters.reduce((v, f) => (f.limit ? Math.min(v, f.limit) : v), Infinity);
let count = 0;
if (sub.onevent) {
const events = this.events.getSortedEvents();
for (const event of events) {
if (matchFilters(sub.filters, event)) {
sub.onevent(event);
count++;
}
if (count === limit) break;
}
}
this.log(`Ran ${sub.id} and got ${count} events`, sub.filters);
if (sub.oneose) sub.oneose();
this.store.add(event);
return "";
}
subscribe(filters: Filter[], options: SubscriptionOptions) {
let stream: ZenObservable.Subscription | undefined = undefined;
const sub: Subscription = {
id: nanoid(8),
filters,
...options,
fire: () => {
this.executeSubscription(sub);
if (stream) stream.unsubscribe();
stream = this.store.stream(filters).subscribe((event) => sub.onevent?.(event));
if (sub.oneose) sub.oneose();
},
close: () => {
this.subscriptions.delete(sub.id);
if (stream) stream.unsubscribe();
},
};
this.subscriptions.set(sub.id, sub);
setTimeout(() => {
this.executeSubscription(sub);
sub.fire();
}, 0);
return sub;
}
@ -75,10 +52,6 @@ export default class MemoryRelay implements SimpleRelay {
id?: string | null;
},
) {
let count = 0;
for (const [id, event] of this.events.events) {
if (matchFilters(filters, event)) count++;
}
return count;
return this.store.database.getForFilters(filters).size;
}
}

View File

@ -3,11 +3,11 @@ import { Debugger } from "debug";
import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import Observable from "zen-observable";
import MultiSubscription from "./multi-subscription";
import { PersistentSubject } from "./subject";
import { logger } from "../helpers/debug";
import EventStore from "./event-store";
import { isReplaceable } from "../helpers/nostr/event";
import replaceableEventsService from "../services/replaceable-events";
import { mergeFilter, isFilterEqual } from "../helpers/nostr/filter";
@ -18,19 +18,17 @@ import relayPoolService from "../services/relay-pool";
import Process from "./process";
import AlignHorizontalCentre02 from "../components/icons/align-horizontal-centre-02";
import processManager from "../services/process-manager";
import { eventStore } from "../services/event-store";
import { eventStore, queryStore } from "../services/event-store";
const BLOCK_SIZE = 100;
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
export type EventFilter = (event: NostrEvent) => boolean;
export default class TimelineLoader {
cursor = dayjs().unix();
filters: Filter[] = [];
relays: AbstractRelay[] = [];
events: EventStore;
timeline = new PersistentSubject<NostrEvent[]>([]);
loading = new PersistentSubject(false);
complete = new PersistentSubject(false);
@ -39,6 +37,8 @@ export default class TimelineLoader {
useCache = true;
name: string;
/** @deprecated */
timeline?: Observable<NostrEvent[]>;
process: Process;
private log: Debugger;
private subscription: MultiSubscription;
@ -53,28 +53,22 @@ export default class TimelineLoader {
this.process.icon = AlignHorizontalCentre02;
this.log = logger.extend("TimelineLoader:" + name);
this.events = new EventStore(name);
this.events.connect(replaceableEventsService.events, false);
this.subscription = new MultiSubscription(name);
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
this.subscription.onCacheEvent.subscribe((event) => this.handleEvent(event, true));
this.process.addChild(this.subscription.process);
// update the timeline when there are new events
this.events.onEvent.subscribe(this.throttleUpdateTimeline.bind(this));
this.events.onDelete.subscribe(this.throttleUpdateTimeline.bind(this));
this.events.onClear.subscribe(this.throttleUpdateTimeline.bind(this));
processManager.registerProcess(this.process);
}
private throttleUpdateTimeline = _throttle(this.updateTimeline, 10);
private updateTimeline() {
this.timeline = queryStore.timeline(this.filters);
if (this.eventFilter) {
const filter = this.eventFilter;
this.timeline.next(this.events.getSortedEvents().filter((e) => filter(e, this.events)));
} else this.timeline.next(this.events.getSortedEvents());
// add filter
this.timeline = this.timeline.map((events) => events.filter((e) => this.eventFilter!(e)));
}
}
private seenInCache = new Set<string>();
@ -82,9 +76,9 @@ export default class TimelineLoader {
// if this is a replaceable event, mirror it over to the replaceable event service
if (isReplaceable(event.kind)) replaceableEventsService.handleEvent(event);
eventStore.add(event);
event = eventStore.add(event);
this.events.addEvent(event);
// publish to local relay
if (!fromCache && this.useCache && localRelay && !this.seenInCache.has(event.id)) localRelay.publish(event);
if (fromCache) this.seenInCache.add(event.id);
@ -98,13 +92,12 @@ export default class TimelineLoader {
private connectToChunkLoader(loader: ChunkedRequest) {
this.process.addChild(loader.process);
this.events.connect(loader.events);
const subs = this.chunkLoaderSubs.get(loader);
subs.push(loader.onChunkFinish.subscribe(this.handleChunkFinished.bind(this)));
}
private disconnectFromChunkLoader(loader: ChunkedRequest) {
loader.destroy();
this.events.disconnect(loader.events);
const subs = this.chunkLoaderSubs.get(loader);
for (const sub of subs) sub.unsubscribe();
this.chunkLoaderSubs.delete(loader);
@ -144,6 +137,9 @@ export default class TimelineLoader {
// update the live subscription query map and add limit
this.subscription.setFilters(mergeFilter(filters, { limit: BLOCK_SIZE / 2 }));
// update timeline
this.updateTimeline();
}
setRelays(relays: Iterable<string | URL | AbstractRelay>) {
@ -256,8 +252,7 @@ export default class TimelineLoader {
}
forgetEvents() {
this.events.clear();
this.timeline.next([]);
this.timeline = undefined;
this.subscription.forgetEvents();
}
reset() {
@ -280,7 +275,6 @@ export default class TimelineLoader {
this.subscription.destroy();
this.events.cleanup();
this.process.remove();
processManager.unregisterProcess(this.process);
}

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import {
Box,
Button,
@ -26,7 +26,6 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSingleEvent from "../../hooks/use-single-event";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { useReadRelays } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import { Kind0ParsedContent, getDisplayName, parseMetadataContent } from "../../helpers/nostr/user-metadata";
import { MetadataAvatar } from "../user/user-avatar";
import HoverLinkOverlay from "../hover-link-overlay";
@ -96,15 +95,18 @@ export default function AppHandlerModal({
const kind = event?.kind ?? getKindFromDecoded(decoded);
const alt = event?.tags.find((t) => t[0] === "alt")?.[1];
const address = encodeDecodeResult(decoded);
const timeline = useTimelineLoader(
const eventFilter = useCallback((event: NostrEvent) => {
return event.content.length > 0;
}, []);
const { loader, timeline: apps } = useTimelineLoader(
`${kind}-apps`,
readRelays,
kind ? { kinds: [kinds.Handlerinformation], "#k": [String(kind)] } : { kinds: [kinds.Handlerinformation] },
{ eventFilter },
);
const autofocus = useBreakpointValue({ base: false, lg: true });
const [search, setSearch] = useState("");
const apps = useSubject(timeline.timeline).filter((a) => a.content.length > 0);
const filteredApps = apps.filter((app) => {
if (search.length > 1) {
@ -117,7 +119,7 @@ export default function AppHandlerModal({
return false;
} else return true;
});
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl">

View File

@ -5,7 +5,6 @@ import { Link as RouterLink } from "react-router-dom";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useCurrentAccount from "../../../hooks/use-current-account";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import useSubject from "../../../hooks/use-subject";
import TimelineActionAndStatus from "../../timeline/timeline-action-and-status";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import Timestamp from "../../timestamp";
@ -98,18 +97,17 @@ export default function GhostTimeline({ ...props }: Omit<FlexProps, "children">)
const account = useCurrentAccount()!;
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${account.pubkey}-ghost`, readRelays, { authors: [account.pubkey] });
const events = useSubject(timeline.timeline);
const { loader, timeline: events } = useTimelineLoader(`${account.pubkey}-ghost`, readRelays, {
authors: [account.pubkey],
});
const callback = useTimelineCurserIntersectionCallback(timeline);
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={timeline} />
{events?.map((event) => <TimelineItem key={event.id} event={event} />)}
<TimelineActionAndStatus timeline={loader} />
</Flex>
</IntersectionObserverProvider>
);

View File

@ -26,7 +26,7 @@ export default function RelayListButton({ relay, ...props }: { relay: string } &
const account = useCurrentAccount();
const [isLoading, setLoading] = useState(false);
const sets = useUserRelaySets(account?.pubkey);
const sets = useUserRelaySets(account?.pubkey) ?? [];
const inSets = sets.filter((set) => set.tags.some((t) => isRTag(t) && t[1] === relay));

View File

@ -110,7 +110,7 @@ export default function RelayManagementDrawer({ isOpen, onClose, ...props }: Omi
const save = useDisclosure();
const [selected, setSelected] = useState<string>();
const relaySets = useUserRelaySets(account?.pubkey);
const relaySets = useUserRelaySets(account?.pubkey) ?? [];
const changeSet = (cord: string) => {
setSelected(cord);

View File

@ -24,8 +24,8 @@ export function SaveRelaySetForm({
const publish = usePublishEvent();
const { register, formState, handleSubmit } = useForm({
defaultValues: {
name: relaySet ? getListName(relaySet) ?? "" : "",
description: relaySet ? getListDescription(relaySet) ?? "" : "",
name: relaySet ? (getListName(relaySet) ?? "") : "",
description: relaySet ? (getListDescription(relaySet) ?? "") : "",
},
mode: "all",
resetOptions: { keepDirtyValues: true },

View File

@ -4,8 +4,6 @@ import { NostrEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import dayjs from "dayjs";
import useSubject from "../../../hooks/use-subject";
import TimelineLoader from "../../../classes/timeline-loader";
import useNumberCache from "../../../hooks/timeline/use-number-cache";
import useCacheEntryHeight from "../../../hooks/timeline/use-cache-entry-height";
import { useTimelineDates } from "../../../hooks/timeline/use-timeline-dates";
@ -15,8 +13,7 @@ import TimelineItem from "./timeline-item";
const INITIAL_NOTES = 10;
const NOTE_BUFFER = 5;
function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
const events = useSubject(timeline.timeline);
function GenericNoteTimeline({ timeline }: { timeline: NostrEvent[] }) {
const [latest, setLatest] = useState(() => dayjs().unix());
const cacheKey = useTimelineLocationCacheKey();
@ -28,7 +25,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
const newNotes: NostrEvent[] = [];
const notes: NostrEvent[] = [];
for (const note of events) {
for (const note of timeline) {
if (note.created_at > latest) newNotes.push(note);
else if (note.created_at >= dates.cursor) notes.push(note);
}
@ -38,7 +35,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
{newNotes.length > 0 && (
<Box h="0" overflow="visible" w="full" zIndex={100} display="flex" position="relative">
<Button
onClick={() => setLatest(timeline.timeline.value[0].created_at + 10)}
onClick={() => setLatest(newNotes[newNotes.length - 1].created_at + 10)}
colorScheme="primary"
size="lg"
mx="auto"

View File

@ -29,11 +29,15 @@ export function useTimelinePageEventFilter() {
export type TimelineViewType = "timeline" | "images" | "health";
export default function TimelinePage({
loader,
timeline,
header,
...props
}: { timeline: TimelineLoader; header?: React.ReactNode } & Omit<FlexProps, "children" | "direction" | "gap">) {
const callback = useTimelineCurserIntersectionCallback(timeline);
}: { loader: TimelineLoader; timeline: NostrEvent[]; header?: React.ReactNode } & Omit<
FlexProps,
"children" | "direction" | "gap"
>) {
const callback = useTimelineCurserIntersectionCallback(loader);
const viewParam = useRouteSearchValue("view", "timeline");
const mode = (viewParam.value as TimelineViewType) ?? "timeline";
@ -47,7 +51,7 @@ export default function TimelinePage({
return <MediaTimeline timeline={timeline} />;
case "health":
return <TimelineHealth timeline={timeline} />;
return <TimelineHealth loader={loader} timeline={timeline} />;
default:
return null;
}
@ -57,7 +61,7 @@ export default function TimelinePage({
<Flex direction="column" gap="2" {...props}>
{header}
{renderTimeline()}
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
</IntersectionObserverProvider>
);

View File

@ -2,8 +2,6 @@ import { useMemo } from "react";
import { kinds } from "nostr-tools";
import { Photo } from "react-photo-album";
import TimelineLoader from "../../../classes/timeline-loader";
import useSubject from "../../../hooks/use-subject";
import { getMatchLink } from "../../../helpers/regexp";
import { LightboxProvider } from "../../lightbox-provider";
import { isImageURL } from "../../../helpers/url";
@ -36,13 +34,11 @@ function ImageGallery({ images }: { images: PhotoWithEvent[] }) {
);
}
export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }) {
const events = useSubject(timeline.timeline);
export default function MediaTimeline({ timeline }: { timeline: NostrEvent[] }) {
const images = useMemo(() => {
var images: PhotoWithEvent[] = [];
for (const event of events) {
for (const event of timeline) {
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) continue;
const urls = event.content.matchAll(getMatchLink());
@ -53,7 +49,7 @@ export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }
}
return images;
}, [events]);
}, [timeline]);
return (
<LightboxProvider>

View File

@ -16,7 +16,6 @@ import {
} from "@chakra-ui/react";
import TimelineLoader from "../../../classes/timeline-loader";
import useSubject from "../../../hooks/use-subject";
import { NostrEvent } from "../../../types/nostr-event";
import { RelayFavicon } from "../../relay-favicon";
import { NoteLink } from "../../note/note-link";
@ -24,6 +23,7 @@ import { BroadcastEventIcon } from "../../icons";
import Timestamp from "../../timestamp";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import { getSeenRelays } from "applesauce-core/helpers";
function EventRow({
event,
@ -31,7 +31,6 @@ function EventRow({
...props
}: { event: NostrEvent; relays: string[] } & Omit<TableRowProps, "children">) {
// const sub = useMemo(() => getEventRelays(event.id), [event.id]);
const seenRelays = true; //useSubject(sub);
const publish = usePublishEvent();
const ref = useEventIntersectionRef(event);
@ -64,7 +63,7 @@ function EventRow({
{broadcasting ? <Spinner size="xs" /> : <BroadcastEventIcon />}
</Td>
{relays.map((relay) => (
<Td key={relay} title={relay} p="2" backgroundColor={/*seenRelays.includes(relay)*/ true ? yes : no}>
<Td key={relay} title={relay} p="2" backgroundColor={getSeenRelays(event)?.has(relay) ? yes : no}>
<RelayFavicon relay={relay} size="2xs" />
</Td>
))}
@ -72,9 +71,8 @@ function EventRow({
);
}
export default function TimelineHealth({ timeline }: { timeline: TimelineLoader }) {
const events = useSubject(timeline.timeline);
const relays = Array.from(Object.keys(timeline.relays));
export default function TimelineHealth({ timeline, loader }: { loader: TimelineLoader; timeline: NostrEvent[] }) {
const relays = Array.from(Object.keys(loader.relays));
return (
<>
@ -102,7 +100,7 @@ export default function TimelineHealth({ timeline }: { timeline: TimelineLoader
</Tr>
</Thead>
<Tbody>
{events.map((event) => (
{timeline.map((event) => (
<EventRow key={event.id} event={event} relays={relays} />
))}
</Tbody>

View File

@ -265,8 +265,8 @@ export function getSortedKinds(events: NostrEvent[]) {
.reduce((dir, k) => ({ ...dir, [k.kind]: k.count }), {} as Record<string, number>);
}
export function getTagValue(event: NostrEvent, tag: string){
return event.tags.find(t => t[0]===tag && t.length>=2)?.[1]
export function getTagValue(event: NostrEvent, tag: string) {
return event.tags.find((t) => t[0] === tag && t.length >= 2)?.[1];
}
export { getEventUID };

View File

@ -1,10 +1,12 @@
import dayjs from "dayjs";
import { kinds } from "nostr-tools";
import { DecodeResult } from "nostr-tools/nip19";
import { getPointerFromTag } from "applesauce-core/helpers";
import { NostrEvent, isRTag } from "../../types/nostr-event";
export const GOAL_KIND = 9041;
/** @deprecated use kinds.ZapGoal */
export const GOAL_KIND = kinds.ZapGoal;
export type ParsedGoal = {
event: NostrEvent;

View File

@ -22,7 +22,7 @@ export function getPageForks(page: NostrEvent) {
const addressFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "fork");
const eventFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "fork");
const address = addressFork ? parseCoordinate(addressFork[1], true) ?? undefined : undefined;
const address = addressFork ? (parseCoordinate(addressFork[1], true) ?? undefined) : undefined;
const event: nip19.EventPointer | undefined = eventFork ? { id: eventFork[1] } : undefined;
return { event, address };
@ -32,7 +32,7 @@ export function getPageDefer(page: NostrEvent) {
const addressTag = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "defer");
const eventTag = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "defer");
const address = addressTag ? parseCoordinate(addressTag[1], true) ?? undefined : undefined;
const address = addressTag ? (parseCoordinate(addressTag[1], true) ?? undefined) : undefined;
const event: nip19.EventPointer | undefined = eventTag ? { id: eventTag[1] } : undefined;
if (event || address) return { event, address };

View File

@ -1,11 +1,11 @@
import { useEffect } from "react";
import TimelineLoader from "../../classes/timeline-loader";
import useMinNumber from "./use-min-number";
import { NumberCache } from "./use-number-cache";
import useTimelineViewDatesBuffer from "./use-timeline-view-dates-buffer";
export function useTimelineDates(
timeline: { id: string; created_at: number }[] | TimelineLoader,
timeline: { id: string; created_at: number }[],
cache: NumberCache,
buffer = 5,
initialRender = 10,
@ -13,7 +13,7 @@ export function useTimelineDates(
const dates = useTimelineViewDatesBuffer(
cache.key,
{ min: cache.get("min"), max: cache.get("max") },
Array.isArray(timeline) ? timeline : timeline.timeline.value,
timeline,
buffer,
initialRender,
);

View File

@ -2,13 +2,12 @@ import { kinds } from "nostr-tools";
import useTimelineLoader from "./use-timeline-loader";
import { recommendedReadRelays } from "../services/client-relays";
import useSubject from "./use-subject";
export default function useNip05Providers() {
const timeline = useTimelineLoader("nip05-providers", recommendedReadRelays, {
const { timeline } = useTimelineLoader("nip05-providers", recommendedReadRelays, {
kinds: [kinds.Handlerinformation],
"#k": [String(kinds.NostrConnect)],
});
return useSubject(timeline.timeline);
return timeline;
}

View File

@ -1,7 +1,6 @@
import { useEffect, useMemo } from "react";
import { kinds as eventKinds } from "nostr-tools";
import useSubject from "./use-subject";
import useSingleEvent from "./use-single-event";
import singleEventService from "../services/single-event";
import useTimelineLoader from "./use-timeline-loader";
@ -21,7 +20,7 @@ export default function useThreadTimelineLoader(
const kindArr = kinds ? (kinds.length > 0 ? kinds : undefined) : [eventKinds.ShortTextNote];
const timelineId = `${rootPointer?.id}-thread`;
const timeline = useTimelineLoader(
const { loader, timeline: events } = useTimelineLoader(
timelineId,
readRelays,
rootPointer
@ -38,8 +37,6 @@ export default function useThreadTimelineLoader(
: undefined,
);
const events = useSubject(timeline.timeline);
// mirror all events to single event cache
useEffect(() => {
for (const e of events) singleEventService.handleEvent(e);
@ -53,5 +50,5 @@ export default function useThreadTimelineLoader(
return arr;
}, [events, rootEvent, focusedEvent]);
return { events: allEvents, rootEvent, rootPointer, timeline };
return { events: allEvents, rootEvent, rootPointer, timeline: loader };
}

View File

@ -3,6 +3,7 @@ import { NostrEvent } from "nostr-tools";
import TimelineLoader from "../classes/timeline-loader";
import { useCachedIntersectionMapCallback } from "../providers/local/intersection-observer";
import { eventStore } from "../services/event-store";
export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader) {
// if the cursor is set too far ahead and the last block did not overlap with the cursor
@ -17,7 +18,7 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader)
let oldestEvent: NostrEvent | undefined = undefined;
for (const [id, entry] of map) {
if (!entry.isIntersecting) continue;
const event = timeline.events.getEvent(id);
const event = eventStore.getEvent(id);
if (!event) continue;
if (!oldestEvent || event.created_at < oldestEvent.created_at) {
oldestEvent = event;

View File

@ -1,17 +1,15 @@
import { useEffect, useMemo } from "react";
import { usePrevious, useUnmount } from "react-use";
import { Filter, NostrEvent } from "nostr-tools";
import { Filter } from "nostr-tools";
import timelineCacheService from "../services/timeline-cache";
import { EventFilter } from "../classes/timeline-loader";
import TimelineLoader, { EventFilter } from "../classes/timeline-loader";
import { useStoreQuery } from "applesauce-react";
import { Queries } from "applesauce-core";
type Options = {
/** @deprecated */
enabled?: boolean;
eventFilter?: EventFilter;
useCache?: boolean;
cursor?: number;
customSort?: (a: NostrEvent, b: NostrEvent) => number;
};
export default function useTimelineLoader(
@ -20,56 +18,46 @@ export default function useTimelineLoader(
filters: Filter | Filter[] | undefined,
opts?: Options,
) {
const timeline = useMemo(() => timelineCacheService.createTimeline(key), [key]);
const loader = useMemo(() => timelineCacheService.createTimeline(key), [key]);
// set use cache
if (opts?.useCache !== undefined) timeline.useCache = opts?.useCache;
if (opts?.useCache !== undefined) loader.useCache = opts?.useCache;
// update relays
useEffect(() => {
timeline.setRelays(relays);
timeline.triggerChunkLoad();
loader.setRelays(relays);
loader.triggerChunkLoad();
}, [Array.from(relays).join("|")]);
// update filters
useEffect(() => {
if (filters) {
timeline.setFilters(Array.isArray(filters) ? filters : [filters]);
timeline.open();
timeline.triggerChunkLoad();
} else timeline.close();
}, [timeline, JSON.stringify(filters)]);
loader.setFilters(Array.isArray(filters) ? filters : [filters]);
loader.open();
loader.triggerChunkLoad();
} else loader.close();
}, [loader, JSON.stringify(filters)]);
// update event filter
useEffect(() => {
timeline.setEventFilter(opts?.eventFilter);
}, [timeline, opts?.eventFilter]);
// update cursor
// NOTE: I don't think this is used anywhere and should be removed
useEffect(() => {
if (opts?.cursor !== undefined) {
timeline.setCursor(opts.cursor);
}
}, [timeline, opts?.cursor]);
// update custom sort
useEffect(() => {
timeline.events.customSort = opts?.customSort;
}, [timeline, opts?.customSort]);
loader.setEventFilter(opts?.eventFilter);
}, [loader, opts?.eventFilter]);
// close the old timeline when the key changes
const oldTimeline = usePrevious(timeline);
const oldTimeline = usePrevious(loader);
useEffect(() => {
if (oldTimeline && oldTimeline !== timeline) {
if (oldTimeline && oldTimeline !== loader) {
oldTimeline.close();
}
}, [timeline, oldTimeline]);
}, [loader, oldTimeline]);
// stop the loader when unmount
useUnmount(() => {
timeline.close();
loader.close();
});
return timeline;
let timeline = useStoreQuery(Queries.TimelineQuery, filters && [filters]) ?? [];
if (opts?.eventFilter) timeline = timeline.filter(opts.eventFilter);
return { loader, timeline };
}

View File

@ -2,7 +2,6 @@ import { useCallback } from "react";
import { NOTE_LIST_KIND, PEOPLE_LIST_KIND, isJunkList } from "../helpers/nostr/lists";
import { useReadRelays } from "./use-client-relays";
import useSubject from "./use-subject";
import useTimelineLoader from "./use-timeline-loader";
import { NostrEvent } from "../types/nostr-event";
import { truncateId } from "../helpers/string";
@ -12,18 +11,18 @@ export default function useUserLists(pubkey?: string, additionalRelays?: Iterabl
const eventFilter = useCallback((event: NostrEvent) => {
return !isJunkList(event);
}, []);
const timeline = useTimelineLoader(
const { timeline } = useTimelineLoader(
`${truncateId(pubkey ?? "anon")}-lists`,
readRelays,
pubkey
? {
authors: pubkey ? [pubkey] : [],
authors: [pubkey],
kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND],
}
: undefined,
{ eventFilter },
);
const lists = useSubject(timeline.timeline);
return pubkey ? lists : [];
return timeline;
}

View File

@ -1,8 +1,9 @@
import { useCallback } from "react";
import { kinds } from "nostr-tools";
import { useStoreQuery } from "applesauce-react";
import { Queries } from "applesauce-core";
import { useReadRelays } from "./use-client-relays";
import useSubject from "./use-subject";
import useTimelineLoader from "./use-timeline-loader";
import { NostrEvent, isRTag } from "../types/nostr-event";
import { truncateId } from "../helpers/string";
@ -10,18 +11,16 @@ import { truncateId } from "../helpers/string";
export default function useUserRelaySets(pubkey?: string, additionalRelays?: Iterable<string>) {
const readRelays = useReadRelays(additionalRelays);
const eventFilter = useCallback((event: NostrEvent) => event.tags.some(isRTag), []);
const timeline = useTimelineLoader(
`${truncateId(pubkey || "anon")}-relay-sets`,
readRelays,
pubkey
? {
authors: pubkey ? [pubkey] : [],
kinds: [kinds.Relaysets],
}
: undefined,
{ eventFilter },
);
const lists = useSubject(timeline.timeline);
return pubkey ? lists : [];
const filters = pubkey
? {
authors: [pubkey],
kinds: [kinds.Relaysets],
}
: undefined;
const { timeline } = useTimelineLoader(`${truncateId(pubkey || "anon")}-relay-sets`, readRelays, filters, {
eventFilter,
});
return timeline;
}

View File

@ -35,7 +35,7 @@ export default function DMTimelineProvider({ children }: PropsWithChildren) {
[userMuteFilter],
);
const timeline = useTimelineLoader(
const { loader } = useTimelineLoader(
`${truncateId(account?.pubkey ?? "anon")}-dms`,
inbox ?? [],
account?.pubkey
@ -47,7 +47,7 @@ export default function DMTimelineProvider({ children }: PropsWithChildren) {
{ eventFilter },
);
const context = useMemo(() => ({ timeline }), [timeline]);
const context = useMemo(() => ({ timeline: loader }), [loader]);
return <DMTimelineContext.Provider value={context}>{children}</DMTimelineContext.Provider>;
}

View File

@ -40,7 +40,7 @@ export default function NotificationsProvider({ children }: PropsWithChildren) {
[userMuteFilter],
);
const timeline = useTimelineLoader(
const { loader } = useTimelineLoader(
`${truncateId(account?.pubkey ?? "anon")}-notification`,
readRelays,
account?.pubkey
@ -74,7 +74,7 @@ export default function NotificationsProvider({ children }: PropsWithChildren) {
};
}, [account?.pubkey]);
const context = useMemo(() => ({ timeline, notifications }), [timeline, notifications]);
const context = useMemo(() => ({ timeline: loader, notifications }), [loader, notifications]);
return <NotificationTimelineContext.Provider value={context}>{children}</NotificationTimelineContext.Provider>;
}

View File

@ -1,8 +1,9 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
import { NostrEvent } from "nostr-tools";
import { useObservable } from "applesauce-react";
import TimelineLoader from "../../classes/timeline-loader";
import useSubject from "../../hooks/use-subject";
import { eventStore } from "../../services/event-store";
export type Thread = {
root?: NostrEvent;
@ -25,7 +26,7 @@ export function useThreadsContext() {
}
export default function ThreadsProvider({ timeline, children }: { timeline: TimelineLoader } & PropsWithChildren) {
const messages = useSubject(timeline.timeline);
const messages = useObservable(timeline.timeline) ?? [];
const threads = useMemo(() => {
const grouped: Record<string, Thread> = {};
@ -36,21 +37,18 @@ export default function ThreadsProvider({ timeline, children }: { timeline: Time
grouped[rootId] = {
messages: [],
rootId,
root: timeline.events.getEvent(rootId),
root: eventStore.getEvent(rootId),
};
}
grouped[rootId].messages.push(message);
}
}
return grouped;
}, [messages.length, timeline.events]);
}, [messages.length]);
const getRoot = useCallback(
(id: string) => {
return timeline.events.getEvent(id);
},
[timeline.events],
);
const getRoot = useCallback((id: string) => {
return eventStore.getEvent(id);
}, []);
const context = useMemo(() => ({ threads, getRoot }), [threads, getRoot]);

View File

@ -31,7 +31,7 @@ export function TrustProvider({
const context = useMemo(() => {
const trust = parentTrust || isEventTrusted;
return {
trust: allowOverride ? override ?? trust : trust,
trust: allowOverride ? (override ?? trust) : trust,
setOverride: (v: boolean) => allowOverride && setOverride(v),
};
}, [override, parentTrust, isEventTrusted, setOverride, allowOverride]);

View File

@ -1,5 +1,6 @@
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { EventStore } from "applesauce-core";
import { WIKI_PAGE_KIND } from "../helpers/nostr/wiki";
import { logger } from "../helpers/debug";
@ -11,15 +12,17 @@ import BookOpen01 from "../components/icons/book-open-01";
import processManager from "./process-manager";
import { localRelay } from "./local-relay";
import relayPoolService from "./relay-pool";
import { eventStore } from "./event-store";
class DictionaryService {
log = logger.extend("DictionaryService");
process: Process;
store: EventStore;
topics = new SuperMap<string, Subject<Map<string, NostrEvent>>>(() => new Subject<Map<string, NostrEvent>>());
loaders = new SuperMap<AbstractRelay, BatchIdentifierLoader>((relay) => {
const loader = new BatchIdentifierLoader(relay, [WIKI_PAGE_KIND], this.log.extend(relay.url));
const loader = new BatchIdentifierLoader(this.store, relay, [WIKI_PAGE_KIND], this.log.extend(relay.url));
this.process.addChild(loader.process);
loader.onIdentifierUpdate.subscribe((identifier) => {
this.updateSubject(identifier);
@ -27,7 +30,8 @@ class DictionaryService {
return loader;
});
constructor() {
constructor(store: EventStore) {
this.store = store;
this.process = new Process("DictionaryService", this);
this.process.icon = BookOpen01;
this.process.active = true;
@ -75,12 +79,15 @@ class DictionaryService {
}
handleEvent(event: NostrEvent) {
event = this.store.add(event);
// pretend it came from the local relay
// TODO: remove this once DictionaryService uses subscriptions from event store
if (localRelay) this.loaders.get(localRelay as AbstractRelay).handleEvent(event);
}
}
const dictionaryService = new DictionaryService();
const dictionaryService = new DictionaryService(eventStore);
if (import.meta.env.DEV) {
// @ts-expect-error

View File

@ -1,9 +1,9 @@
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import { EventStore } from "applesauce-core";
import SuperMap from "../classes/super-map";
import EventStore from "../classes/event-store";
import Subject from "../classes/subject";
import BatchKindPubkeyLoader, { createCoordinate } from "../classes/batch-kind-pubkey-loader";
import Process from "../classes/process";
@ -29,31 +29,34 @@ export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: str
}
class ReplaceableEventsService {
store: EventStore;
process: Process;
/** @deprecated */
private subjects = new SuperMap<string, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
cacheLoader: BatchKindPubkeyLoader | null = null;
loaders = new SuperMap<AbstractRelay, BatchKindPubkeyLoader>((relay) => {
const loader = new BatchKindPubkeyLoader(relay, this.log.extend(relay.url));
loader.events.onEvent.subscribe((e) => this.handleEvent(e));
const loader = new BatchKindPubkeyLoader(this.store, relay, this.log.extend(relay.url));
this.process.addChild(loader.process);
return loader;
});
events = new EventStore();
log = logger.extend("ReplaceableEventLoader");
constructor() {
constructor(store: EventStore) {
this.store = store;
this.process = new Process("ReplaceableEventsService", this);
this.process.icon = UserSquare;
this.process.active = true;
processManager.registerProcess(this.process);
if (localRelay) {
this.cacheLoader = new BatchKindPubkeyLoader(localRelay as AbstractRelay, this.log.extend("cache-relay"));
this.cacheLoader.events.onEvent.subscribe((e) => this.handleEvent(e, true));
this.cacheLoader = new BatchKindPubkeyLoader(
this.store,
localRelay as AbstractRelay,
this.log.extend("cache-relay"),
);
this.process.addChild(this.cacheLoader.process);
}
}
@ -61,15 +64,14 @@ class ReplaceableEventsService {
private seenInCache = new Set<string>();
handleEvent(event: NostrEvent, fromCache = false) {
if (!fromCache && !alwaysVerify(event)) return;
const cord = getEventCoordinate(event);
event = this.store.add(event);
eventStore.add(event);
const cord = getEventCoordinate(event);
const subject = this.subjects.get(cord);
const current = subject.value;
if (!current || event.created_at > current.created_at) {
subject.next(event);
this.events.addEvent(event);
if (!fromCache && localRelay && !this.seenInCache.has(event.id)) localRelay.publish(event);
}
@ -125,7 +127,7 @@ class ReplaceableEventsService {
}
}
const replaceableEventsService = new ReplaceableEventsService();
const replaceableEventsService = new ReplaceableEventsService(eventStore);
if (import.meta.env.DEV) {
//@ts-ignore

View File

@ -1,53 +1,40 @@
import _throttle from "lodash.throttle";
import { EventStore } from "applesauce-core";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { localRelay } from "./local-relay";
import { logger } from "../helpers/debug";
import Subject from "../classes/subject";
import relayPoolService from "./relay-pool";
import Process from "../classes/process";
import processManager from "./process-manager";
import Code02 from "../components/icons/code-02";
import BatchEventLoader from "../classes/batch-event-loader";
import EventStore from "../classes/event-store";
import { eventStore } from "./event-store";
class SingleEventService {
process: Process;
store: EventStore;
log = logger.extend("SingleEventService");
// events = new EventStore();
// subjects = new SuperMap<string, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
loaders = new SuperMap<AbstractRelay, BatchEventLoader>((relay) => {
const loader = new BatchEventLoader(relay, this.log.extend(relay.url));
const loader = new BatchEventLoader(this.store, relay, this.log.extend(relay.url));
this.process.addChild(loader.process);
// this.events.connect(loader.events);
return loader;
});
pendingRelays = new SuperMap<string, Set<AbstractRelay>>(() => new Set());
idsFromRelays = new SuperMap<AbstractRelay, Set<string>>(() => new Set());
// subscriptions = new Map<AbstractRelay, PersistentSubscription>();
constructor() {
constructor(store: EventStore) {
this.store = store;
this.process = new Process("SingleEventService", this);
this.process.icon = Code02;
this.process.active = true;
processManager.registerProcess(this.process);
// when an event is added to the store, pass it along to the subjects
// this.events.onEvent.subscribe((event) => {
// this.subjects.get(event.id).next(event);
// });
}
// getSubject(id: string) {
// return this.subjects.get(id);
// }
private loadEventFromRelays(id: string) {
const relays = this.pendingRelays.get(id);
@ -58,9 +45,7 @@ class SingleEventService {
loadingFromCache = new Set<string>();
requestEvent(id: string, urls: Iterable<string | URL | AbstractRelay>) {
if (eventStore.hasEvent(id)) return;
// const subject = this.subjects.get(id);
// if (subject.value) return subject;
if (this.store.hasEvent(id)) return;
const relays = relayPoolService.getRelays(urls);
for (const relay of relays) this.pendingRelays.get(id).add(relay);
@ -88,13 +73,13 @@ class SingleEventService {
// this.events.addEvent(event);
this.pendingRelays.delete(event.id);
eventStore.add(event);
event = this.store.add(event);
if (!fromCache && localRelay) localRelay.publish(event);
}
}
const singleEventService = new SingleEventService();
const singleEventService = new SingleEventService(eventStore);
if (import.meta.env.DEV) {
//@ts-expect-error

View File

@ -1,10 +1,12 @@
import _throttle from "lodash.throttle";
import { kinds } from "nostr-tools";
import { getSearchNames } from "../helpers/nostr/user-metadata";
import db from "./db";
import replaceableEventsService from "./replaceable-events";
import userMetadataService from "./user-metadata";
import { logger } from "../helpers/debug";
import Subject from "../classes/subject";
import { eventStore } from "./event-store";
const WRITE_USER_SEARCH_BATCH_TIME = 500;
const log = logger.extend("UsernameSearch");
@ -32,9 +34,7 @@ const writeSearchData = _throttle(async () => {
userSearchUpdate.next(Math.random());
}, WRITE_USER_SEARCH_BATCH_TIME);
replaceableEventsService.events.onEvent.subscribe((event) => {
if (event.kind === 0) {
writeSearchQueue.add(event.pubkey);
writeSearchData();
}
eventStore.stream([{ kinds: [kinds.Metadata] }]).subscribe((event) => {
writeSearchQueue.add(event.pubkey);
writeSearchData();
});

View File

@ -1,16 +1,13 @@
import { useCallback, useMemo } from "react";
import { Filter, kinds, NostrEvent } from "nostr-tools";
import { Button, Flex, Heading, Spacer } from "@chakra-ui/react";
import { Flex, Heading, Spacer } from "@chakra-ui/react";
import { getEventUID } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import VerticalPageLayout from "../../components/vertical-page-layout";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import Plus from "../../components/icons/plus";
import { useReadRelays } from "../../hooks/use-client-relays";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
@ -25,21 +22,23 @@ function ArticlesHomePage() {
const eventFilter = useCallback(
(event: NostrEvent) => {
if (userMuteFilter(event)) return false;
if (!getArticleTitle(event)) return false;
if (!event.content) return false;
return true;
},
[userMuteFilter],
);
const { filter, listId } = usePeopleListContext();
const query = useMemo<Filter[] | undefined>(() => {
const filters = useMemo<Filter[] | undefined>(() => {
if (!filter) return undefined;
return [{ authors: filter.authors, kinds: [kinds.LongFormArticle] }];
}, [filter]);
const timeline = useTimelineLoader(`${listId ?? "global"}-articles`, relays, query, { eventFilter });
const articles = useSubject(timeline.timeline).filter((article) => !!getArticleTitle(article) && !!article.content);
const callback = useTimelineCurserIntersectionCallback(timeline);
const { loader, timeline: articles } = useTimelineLoader(`${listId ?? "global"}-articles`, relays, filters, {
eventFilter,
});
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -56,7 +55,7 @@ function ArticlesHomePage() {
{articles.map((article) => (
<ArticleCard key={getEventUID(article)} article={article} />
))}
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</IntersectionObserverProvider>
</VerticalPageLayout>
);

View File

@ -1,5 +1,6 @@
import { useNavigate } from "react-router-dom";
import { kinds } from "nostr-tools";
import { useObservable } from "applesauce-react";
import {
Button,
Flex,
@ -37,13 +38,13 @@ import { ErrorBoundary } from "../../components/error-boundary";
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) {
const awards = useSubject(timeline.timeline);
const awards = useObservable(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<Flex direction="column" gap="4">
<IntersectionObserverProvider callback={callback}>
{awards.map((award) => (
{awards?.map((award) => (
<ErrorBoundary key={award.id}>
<BadgeAwardCard award={award} showImage={false} />
</ErrorBoundary>
@ -54,13 +55,15 @@ function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) {
}
function BadgeUsersTab({ timeline }: { timeline: TimelineLoader }) {
const awards = useSubject(timeline.timeline);
const awards = useObservable(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const pubkeys = new Set<string>();
for (const award of awards) {
for (const { pubkey } of getBadgeAwardPubkeys(award)) {
pubkeys.add(pubkey);
if (awards) {
for (const award of awards) {
for (const { pubkey } of getBadgeAwardPubkeys(award)) {
pubkeys.add(pubkey);
}
}
}
@ -86,7 +89,7 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
const readRelays = useReadRelays();
const coordinate = getEventCoordinate(badge);
const awardsTimeline = useTimelineLoader(`${coordinate}-awards`, readRelays, {
const { loader } = useTimelineLoader(`${coordinate}-awards`, readRelays, {
"#a": [coordinate],
kinds: [kinds.BadgeAward],
});
@ -141,10 +144,10 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
</TabList>
<TabPanels>
<TabPanel px="0">
<BadgeActivityTab timeline={awardsTimeline} />
<BadgeActivityTab timeline={loader} />
</TabPanel>
<TabPanel>
<BadgeUsersTab timeline={awardsTimeline} />
<BadgeUsersTab timeline={loader} />
</TabPanel>
</TabPanels>
</Tabs>

View File

@ -7,7 +7,6 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import { getEventUID } from "../../helpers/nostr/event";
import BadgeCard from "./components/badge-card";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -16,14 +15,12 @@ function BadgesBrowsePage() {
const { filter, listId } = usePeopleListContext();
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: lists } = useTimelineLoader(
`${listId}-badges`,
readRelays,
filter ? { ...filter, kinds: [kinds.BadgeDefinition] } : undefined,
);
const lists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
@ -33,9 +30,7 @@ function BadgesBrowsePage() {
</Flex>
<SimpleGrid columns={{ base: 1, sm: 2, md: 2, lg: 3, xl: 4 }} spacing="2">
{lists.map((badge) => (
<BadgeCard key={getEventUID(badge)} badge={badge} />
))}
{lists?.map((badge) => <BadgeCard key={getEventUID(badge)} badge={badge} />)}
</SimpleGrid>
</VerticalPageLayout>
</IntersectionObserverProvider>

View File

@ -9,7 +9,6 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import { useReadRelays } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import BadgeAwardCard from "./components/badge-award-card";
@ -28,7 +27,7 @@ function BadgesPage() {
[muteFilter],
);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: awards } = useTimelineLoader(
`${listId}-lists`,
readRelays,
{
@ -37,9 +36,7 @@ function BadgesPage() {
},
{ eventFilter },
);
const awards = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -63,7 +60,7 @@ function BadgesPage() {
<PeopleListSelection />
</Flex>
<IntersectionObserverProvider callback={callback}>
{awards.map((award) => (
{awards?.map((award) => (
<ErrorBoundary key={award.id}>
<BadgeAwardCard award={award} />
</ErrorBoundary>

View File

@ -68,7 +68,7 @@ function ChannelPage({ channel }: { channel: NostrEvent }) {
},
[clientMuteFilter],
);
const timeline = useTimelineLoader(
const { loader, timeline } = useTimelineLoader(
`${truncateId(channel.id)}-chat-messages`,
relays,
{
@ -77,10 +77,10 @@ function ChannelPage({ channel }: { channel: NostrEvent }) {
},
{ eventFilter },
);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<ThreadsProvider timeline={timeline}>
<ThreadsProvider timeline={loader}>
<IntersectionObserverProvider callback={callback}>
<Flex h="full" overflow="hidden" direction="column" p="2" gap="2" flexGrow={1}>
<Flex gap="2" alignItems="center">
@ -106,8 +106,8 @@ function ChannelPage({ channel }: { channel: NostrEvent }) {
py="4"
px="2"
>
<ChannelChatLog timeline={timeline} channel={channel} />
<TimelineActionAndStatus timeline={timeline} />
<ChannelChatLog timeline={loader} channel={channel} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
<ChannelMessageForm channel={channel} />

View File

@ -16,10 +16,10 @@ import {
LinkBox,
Text,
} from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import useChannelMetadata from "../../../hooks/use-channel-metadata";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import UserLink from "../../../components/user/user-link";
@ -42,20 +42,16 @@ function UserCard({ pubkey }: { pubkey: string }) {
);
}
function ChannelMembers({ channel, relays }: { channel: NostrEvent; relays: Iterable<string> }) {
const timeline = useTimelineLoader(`${channel.id}-members`, relays, {
const { loader, timeline: userLists } = useTimelineLoader(`${channel.id}-members`, relays, {
kinds: [CHANNELS_LIST_KIND],
"#e": [channel.id],
});
const userLists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<Flex gap="2" direction="column">
{userLists.map((list) => (
<UserCard key={list.pubkey} pubkey={list.pubkey} />
))}
{userLists?.map((list) => <UserCard key={list.pubkey} pubkey={list.pubkey} />)}
</Flex>
</IntersectionObserverProvider>
);

View File

@ -3,7 +3,6 @@ import { kinds } from "nostr-tools";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import VerticalPageLayout from "../../components/vertical-page-layout";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
@ -27,15 +26,13 @@ function ChannelsHomePage() {
},
[clientMuteFilter],
);
const timeline = useTimelineLoader(
const { loader, timeline: channels } = useTimelineLoader(
`${listId}-channels`,
relays,
filter ? { ...filter, kinds: [kinds.ChannelCreation] } : undefined,
{ eventFilter },
);
const channels = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -44,7 +41,7 @@ function ChannelsHomePage() {
</Flex>
<IntersectionObserverProvider callback={callback}>
<SimpleGrid columns={{ base: 1, xl: 2 }} spacing="2">
{channels.map((channel) => (
{channels?.map((channel) => (
<ErrorBoundary key={channel.id}>
<ChannelCard channel={channel} additionalRelays={relays} />
</ErrorBoundary>

View File

@ -38,7 +38,6 @@ import {
import { getImageSize } from "../../helpers/image";
import { useReadRelays } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useReplaceableEvents from "../../hooks/use-replaceable-events";
@ -88,7 +87,7 @@ function CommunitiesHomePage() {
if (pub) navigate(`/c/${getCommunityName(pub.event)}/${pub.event.pubkey}`);
};
const timeline = useTimelineLoader(
const { loader, timeline: events } = useTimelineLoader(
`all-communities-timeline`,
readRelays,
communityCoordinates.length > 0
@ -111,7 +110,6 @@ function CommunitiesHomePage() {
return Array.from(set);
}, [communities]);
const events = useSubject(timeline.timeline);
const approvalMap = buildApprovalMap(events, mods);
const approved = events
@ -119,7 +117,7 @@ function CommunitiesHomePage() {
.map((event) => ({ event, approvals: approvalMap.get(event.id) }))
.filter((e) => !muteFilter(e.event));
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
const communityDrawer = useDisclosure();
@ -152,7 +150,7 @@ function CommunitiesHomePage() {
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} showCommunity />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
<Flex gap="2" direction="column" w="md" flexShrink={0} hideBelow="xl">
<Heading size="md">Joined Communities</Heading>

View File

@ -1,6 +1,7 @@
import { useContext } from "react";
import { Button, ButtonGroup, Divider, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
import { useObservable } from "applesauce-react";
import { kinds, nip19 } from "nostr-tools";
import {
@ -30,7 +31,6 @@ import { WritingIcon } from "../../components/icons";
import { PostModalContext } from "../../providers/route/post-modal-provider";
import CommunityEditModal from "./components/community-edit-modal";
import TimelineLoader from "../../classes/timeline-loader";
import useSubject from "../../hooks/use-subject";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
function getCommunityPath(community: NostrEvent) {
@ -51,13 +51,13 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
const communityRelays = getCommunityRelays(community);
const readRelays = useReadRelays(communityRelays);
const timeline = useTimelineLoader(`${getEventUID(community)}-timeline`, readRelays, {
const { loader } = useTimelineLoader(`${getEventUID(community)}-timeline`, readRelays, {
kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, COMMUNITY_APPROVAL_KIND],
"#a": [communityCoordinate],
});
// get pending notes
const events = useSubject(timeline.timeline);
const events = useObservable(loader.timeline) ?? [];
const mods = getCommunityMods(community);
const approvals = buildApprovalMap(events, mods);
const pending = events.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && !approvals.has(e.id) && !muteFilter(e));
@ -142,7 +142,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
</Button>
</ButtonGroup>
<Outlet context={{ community, timeline } satisfies RouterContext} />
<Outlet context={{ community, timeline: loader } satisfies RouterContext} />
</Flex>
{!verticalLayout && (

View File

@ -19,7 +19,6 @@ import { getCommunityRelays } from "../../../helpers/nostr/communities";
import { getEventCoordinate } from "../../../helpers/nostr/event";
import { COMMUNITIES_LIST_KIND } from "../../../helpers/nostr/lists";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status";
import UserLink from "../../../components/user/user-link";
@ -45,17 +44,17 @@ export default function CommunityMembersModal({
}: Omit<ModalProps, "children"> & { community: NostrEvent }) {
const communityCoordinate = getEventCoordinate(community);
const readRelays = useReadRelays(getCommunityRelays(community));
const timeline = useTimelineLoader(`${communityCoordinate}-members`, readRelays, [
const { loader, timeline: lists } = useTimelineLoader(`${communityCoordinate}-members`, readRelays, [
{ "#a": [communityCoordinate], kinds: [COMMUNITIES_LIST_KIND] },
]);
const lists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
const listsByPubkey: Record<string, NostrEvent> = {};
for (const list of lists) {
if (!listsByPubkey[list.pubkey] || listsByPubkey[list.pubkey].created_at < list.created_at) {
listsByPubkey[list.pubkey] = list;
if (lists) {
for (const list of lists) {
if (!listsByPubkey[list.pubkey] || listsByPubkey[list.pubkey].created_at < list.created_at) {
listsByPubkey[list.pubkey] = list;
}
}
}
@ -70,11 +69,9 @@ export default function CommunityMembersModal({
<ModalCloseButton />
<ModalBody p="4">
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="4">
{lists.map((list) => (
<UserCard key={list.id} pubkey={list.pubkey} />
))}
{lists?.map((list) => <UserCard key={list.id} pubkey={list.pubkey} />)}
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</ModalBody>
<ModalFooter px="4" pt="0" pb="4">

View File

@ -6,7 +6,6 @@ import { useReadRelays } from "../../hooks/use-client-relays";
import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -27,23 +26,19 @@ export default function CommunityFindByNameView() {
const eventFilter = useCallback((event: NostrEvent) => {
return validateCommunity(event);
}, []);
const timeline = useTimelineLoader(
const { loader, timeline: communities } = useTimelineLoader(
`${community}-find-communities`,
readRelays,
community ? { kinds: [COMMUNITY_DEFINITION_KIND], "#d": [community] } : undefined,
);
const communities = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<VerticalPageLayout>
<Heading>Select Community</Heading>
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
{communities.map((event) => (
<CommunityCard key={getEventUID(event)} community={event} />
))}
{communities?.map((event) => <CommunityCard key={getEventUID(event)} community={event} />)}
</SimpleGrid>
</VerticalPageLayout>
</IntersectionObserverProvider>

View File

@ -8,13 +8,14 @@ import TimelineActionAndStatus from "../../../components/timeline/timeline-actio
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
import ApprovedEvent from "../components/community-approved-post";
import { RouterContext } from "../community-home";
import { useObservable } from "applesauce-react";
export default function CommunityNewestView() {
const { community, timeline } = useOutletContext<RouterContext>();
const muteFilter = useUserMuteFilter();
const mods = getCommunityMods(community);
const events = useSubject(timeline.timeline);
const events = useObservable(timeline.timeline) ?? [];
const approvalMap = buildApprovalMap(events, mods);
const approved = events

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from "react";
import { Button, Flex } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { useObservable } from "applesauce-react";
import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
@ -11,7 +12,6 @@ import {
getCommunityMods,
getCommunityRelays,
} from "../../../helpers/nostr/communities";
import useSubject from "../../../hooks/use-subject";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status";
@ -79,7 +79,7 @@ export default function CommunityPendingView() {
const muteFilter = useUserMuteFilter();
const { community, timeline } = useOutletContext<RouterContext>();
const events = useSubject(timeline.timeline);
const events = useObservable(timeline.timeline) ?? [];
const mods = getCommunityMods(community);
const approvals = buildApprovalMap(events, mods);

View File

@ -1,5 +1,6 @@
import { useMemo } from "react";
import { useOutletContext } from "react-router-dom";
import { useObservable } from "applesauce-react";
import {
COMMUNITY_APPROVAL_KIND,
@ -7,7 +8,6 @@ import {
getCommunityMods,
getCommunityRelays,
} from "../../../helpers/nostr/communities";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status";
@ -22,7 +22,7 @@ export default function CommunityTrendingView() {
const muteFilter = useUserMuteFilter();
const mods = getCommunityMods(community);
const events = useSubject(timeline.timeline);
const events = useObservable(timeline.timeline) ?? [];
const approvalMap = buildApprovalMap(events, mods);
const approved = events

View File

@ -1,5 +1,5 @@
import { useCallback, useMemo } from "react";
import { Divider, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
import { Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
import { Navigate } from "react-router-dom";
import { kinds, NostrEvent } from "nostr-tools";
@ -15,7 +15,6 @@ import KindSelectionProvider, { useKindSelectionContext } from "../../../provide
import NoteFilterTypeButtons from "../../../components/note-filter-type-buttons";
import TimelineViewTypeButtons from "../../../components/timeline-page/timeline-view-type";
import Telescope from "../../../components/icons/telescope";
import UserAvatar from "../../../components/user/user-avatar";
import UserAvatarLink from "../../../components/user/user-avatar-link";
import BackButton from "../../../components/router/back-button";
import UserLink from "../../../components/user/user-link";
@ -53,7 +52,7 @@ function BlindspotFeedPage({ pubkey }: { pubkey: string }) {
);
const { kinds } = useKindSelectionContext();
const timeline = useTimelineLoader(
const { loader, timeline } = useTimelineLoader(
`blnidspot-${account.pubkey}-${pubkey}-${kinds.join(",")}`,
readRelays,
blindspot.length > 0 ? [{ authors: blindspot, kinds }] : undefined,
@ -87,7 +86,7 @@ function BlindspotFeedPage({ pubkey }: { pubkey: string }) {
<TimelineViewTypeButtons />
</Flex>
<TimelinePage timeline={timeline} />
<TimelinePage loader={loader} timeline={timeline} />
</VerticalPageLayout>
);
}

View File

@ -32,7 +32,7 @@ import { usePublishEvent } from "../../../../providers/global/publish-provider";
function NextPageButton({ chain, pointer }: { pointer: AddressPointer; chain: ChainedDVMJob[] }) {
const publish = usePublishEvent();
const dvmRelays = useUserMailboxes(pointer.pubkey)
const dvmRelays = useUserMailboxes(pointer.pubkey);
const readRelays = useReadRelays();
const lastJob = chain[chain.length - 1];

View File

@ -25,7 +25,6 @@ import {
} from "../../../helpers/nostr/dvm";
import { DraftNostrEvent } from "../../../types/nostr-event";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import useSubject from "../../../hooks/use-subject";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useCurrentAccount from "../../../hooks/use-current-account";
@ -49,7 +48,7 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
const dvmRelays = useUserMailboxes(pointer.pubkey)?.outboxes;
const readRelays = useReadRelays(dvmRelays);
const timeline = useTimelineLoader(
const { loader, timeline: events } = useTimelineLoader(
`${getHumanReadableCoordinate(pointer.kind, pointer.pubkey, pointer.identifier)}-jobs`,
readRelays,
[
@ -61,8 +60,6 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
},
],
);
const events = useSubject(timeline.timeline);
const jobs = groupEventsIntoJobs(events);
const pages = chainJobs(Array.from(Object.values(jobs)));
const jobChains = flattenJobChain(pages);

View File

@ -1,12 +1,13 @@
import { useCallback } from "react";
import { Card, Flex, Heading, Link, LinkBox, SimpleGrid, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { kinds, NostrEvent } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import DVMCard from "./dvm-feed/components/dvm-card";
import { DVM_CONTENT_DISCOVERY_JOB_KIND } from "../../helpers/nostr/dvm";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import RequireCurrentAccount from "../../providers/route/require-current-account";
import { getEventCoordinate } from "../../helpers/nostr/event";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
@ -17,14 +18,19 @@ import { RelayIcon } from "../../components/icons";
function DVMFeeds() {
const readRelays = useReadRelays();
const timeline = useTimelineLoader("content-discovery-dvms", readRelays, {
kinds: [31990],
"#k": [String(DVM_CONTENT_DISCOVERY_JOB_KIND)],
});
const DMVs = useSubject(timeline.timeline).filter((e) => !e.tags.some((t) => t[0] === "web"));
const callback = useTimelineCurserIntersectionCallback(timeline);
const eventFilter = useCallback((event: NostrEvent) => {
return !event.tags.some((t) => t[0] === "web");
}, []);
const { loader, timeline: DVMs } = useTimelineLoader(
"content-discovery-dvms",
readRelays,
{
kinds: [kinds.Handlerinformation],
"#k": [String(DVM_CONTENT_DISCOVERY_JOB_KIND)],
},
{ eventFilter },
);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<>
@ -39,7 +45,7 @@ function DVMFeeds() {
</Text>
<IntersectionObserverProvider callback={callback}>
<SimpleGrid columns={{ base: 1, md: 1, lg: 2, xl: 3 }} spacing="2">
{DMVs.map((appData) => (
{DVMs.map((appData) => (
<DVMCard key={appData.id} appData={appData} to={`/discovery/dvm/${getEventCoordinate(appData)}`} />
))}
</SimpleGrid>

View File

@ -6,7 +6,6 @@ import { NostrEvent, kinds } from "nostr-tools";
import { ThreadIcon } from "../../components/icons";
import UserAvatar from "../../components/user/user-avatar";
import UserLink from "../../components/user/user-link";
import useSubject from "../../hooks/use-subject";
import RequireCurrentAccount from "../../providers/route/require-current-account";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useCurrentAccount from "../../hooks/use-current-account";
@ -18,7 +17,6 @@ import SendMessageForm from "./components/send-message-form";
import { groupMessages } from "../../helpers/nostr/dms";
import ThreadDrawer from "./components/thread-drawer";
import ThreadsProvider from "../../providers/local/thread-provider";
import TimelineLoader from "../../classes/timeline-loader";
import DirectMessageBlock from "./components/direct-message-block";
import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer";
import useUserMailboxes from "../../hooks/use-user-mailboxes";
@ -30,8 +28,7 @@ import { BackIconButton } from "../../components/router/back-button";
import decryptionCacheService from "../../services/decryption-cache";
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */
const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => {
const messages = useSubject(timeline.timeline);
const ChatLog = memo(({ messages }: { messages: NostrEvent[] }) => {
const filteredMessages = useMemo(
() => messages.filter((e) => !e.tags.some((t) => t[0] === "e" && t[3] === "root")),
[messages.length],
@ -86,7 +83,7 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
const otherMailboxes = useUserMailboxes(pubkey);
const mailboxes = useUserMailboxes(account.pubkey);
const timeline = useTimelineLoader(
const { loader, timeline: messages } = useTimelineLoader(
`${truncateId(pubkey)}-${truncateId(account.pubkey)}-messages`,
RelaySet.from(mailboxes?.inboxes, mailboxes?.outboxes, otherMailboxes?.inboxes, otherMailboxes?.outboxes),
[
@ -101,7 +98,7 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
const [loading, setLoading] = useState(false);
const decryptAll = async () => {
const promises = timeline.timeline.value
const promises = messages
.map((message) => {
const container = decryptionCacheService.getOrCreateContainer(message.id, "nip04", pubkey, message.content);
return decryptionCacheService.requestDecrypt(container);
@ -112,10 +109,10 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
Promise.all(promises).finally(() => setLoading(false));
};
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<ThreadsProvider timeline={timeline}>
<ThreadsProvider timeline={loader}>
<IntersectionObserverProvider callback={callback}>
<Card size="sm" flexShrink={0} p="2" flexDirection="row">
<Flex gap="2" alignItems="center">
@ -139,8 +136,8 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
</ButtonGroup>
</Card>
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="2" py="4" px="2">
<ChatLog timeline={timeline} />
<TimelineActionAndStatus timeline={timeline} />
<ChatLog messages={messages} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
<SendMessageForm flexShrink={0} pubkey={pubkey} />
{location.state?.thread && (

View File

@ -1,10 +1,10 @@
import { useMemo } from "react";
import { Card, CardBody, Flex, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
import { Outlet, Link as RouterLink, useLocation, useParams } from "react-router-dom";
import { useObservable } from "applesauce-react";
import { nip19 } from "nostr-tools";
import UserAvatar from "../../components/user/user-avatar";
import useSubject from "../../hooks/use-subject";
import RequireCurrentAccount from "../../providers/route/require-current-account";
import Timestamp from "../../components/timestamp";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
@ -66,7 +66,7 @@ function DirectMessagesPage() {
const account = useCurrentAccount()!;
const timeline = useDMTimeline();
const messages = useSubject(timeline.timeline);
const messages = useObservable(timeline.timeline) ?? [];
const conversations = useMemo(() => {
const conversations = groupIntoConversations(messages).map((c) => identifyConversation(c, account.pubkey));
const filtered = conversations.filter((conversation) =>

View File

@ -1,5 +1,6 @@
import { useCallback } from "react";
import { Flex, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react";
import { getEventUID } from "applesauce-core/helpers";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
@ -8,9 +9,7 @@ import { useReadRelays } from "../../hooks/use-client-relays";
import { NostrEvent } from "../../types/nostr-event";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import EmojiPackCard from "./components/emoji-pack-card";
import { getEventUID } from "../../helpers/nostr/event";
import { EMOJI_PACK_KIND, getEmojisFromPack } from "../../helpers/nostr/emoji-packs";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -26,15 +25,13 @@ function EmojiPacksBrowsePage() {
[showEmpty.isOpen],
);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: packs } = useTimelineLoader(
`${listId}-browse-emoji-packs`,
readRelays,
filter ? { ...filter, kinds: [EMOJI_PACK_KIND] } : undefined,
{ eventFilter },
);
const packs = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
@ -47,9 +44,7 @@ function EmojiPacksBrowsePage() {
</Flex>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{packs.map((event) => (
<EmojiPackCard key={getEventUID(event)} pack={event} />
))}
{packs?.map((event) => <EmojiPackCard key={getEventUID(event)} pack={event} />)}
</SimpleGrid>
</VerticalPageLayout>
</IntersectionObserverProvider>

View File

@ -1,5 +1,6 @@
import { Button, Divider, Flex, Heading, Link, SimpleGrid, useDisclosure } from "@chakra-ui/react";
import { Button, Flex, Heading, Link, SimpleGrid, useDisclosure } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { useObservable } from "applesauce-react";
import useCurrentAccount from "../../hooks/use-current-account";
import { ExternalLinkIcon } from "../../components/icons";
@ -7,7 +8,6 @@ import { getEventCoordinate, getEventUID } from "../../helpers/nostr/event";
import { useReadRelays } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs";
import useSubject from "../../hooks/use-subject";
import EmojiPackCard from "./components/emoji-pack-card";
import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs";
import useReplaceableEvents from "../../hooks/use-replaceable-events";
@ -19,7 +19,7 @@ function UserEmojiPackMangerPage() {
const favoritePacks = useFavoriteEmojiPacks(account.pubkey);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: packs } = useTimelineLoader(
`${account.pubkey}-emoji-packs`,
readRelays,
account.pubkey
@ -31,7 +31,7 @@ function UserEmojiPackMangerPage() {
);
const favorites = useReplaceableEvents(favoritePacks && getPackCordsFromFavorites(favoritePacks));
const packs = useSubject(timeline.timeline).filter((pack) => {
const filtered = packs.filter((pack) => {
const cord = getEventCoordinate(pack);
return !favorites.some((e) => getEventCoordinate(e) === cord);
});
@ -50,7 +50,7 @@ function UserEmojiPackMangerPage() {
</SimpleGrid>
</>
)}
{packs.length > 0 && (
{filtered.length > 0 && (
<>
<Heading size="lg" mt="2">
Emoji packs

View File

@ -2,7 +2,6 @@ import { useState } from "react";
import { Flex, Image, SimpleGrid, Spacer, Text } from "@chakra-ui/react";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";
import { FILE_KIND, IMAGE_TYPES, VIDEO_TYPES, getFileUrl, parseImageFile } from "../../helpers/nostr/files";
import { ErrorBoundary } from "../../components/error-boundary";
@ -112,15 +111,12 @@ function FilesPage() {
const [selectedTypes, setSelectedTypes] = useState<string[]>(IMAGE_TYPES);
const timeline = useTimelineLoader(
const { loader, timeline: events } = useTimelineLoader(
`${listId}-files`,
relays,
{ kinds: [FILE_KIND], "#m": selectedTypes, ...filter },
{ enabled: selectedTypes.length > 0 && !!filter },
selectedTypes.length > 0 && !!filter ? { kinds: [FILE_KIND], "#m": selectedTypes, ...filter } : undefined,
);
const events = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -131,14 +127,14 @@ function FilesPage() {
<IntersectionObserverProvider callback={callback}>
<SimpleGrid minChildWidth="20rem" spacing="2">
{events.map((event) => (
{events?.map((event) => (
<ErrorBoundary key={event.id} event={event}>
<FileType event={event} />
</ErrorBoundary>
))}
</SimpleGrid>
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</VerticalPageLayout>
);
}

View File

@ -1,5 +1,6 @@
import { useCallback } from "react";
import { Flex, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react";
import { getEventUID } from "applesauce-core/helpers";
import dayjs from "dayjs";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
@ -8,13 +9,12 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import GoalCard from "./components/goal-card";
import { getEventUID } from "../../helpers/nostr/event";
import { GOAL_KIND, getGoalClosedDate } from "../../helpers/nostr/goal";
import { getGoalClosedDate } from "../../helpers/nostr/goal";
import { NostrEvent } from "../../types/nostr-event";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { ErrorBoundary } from "../../components/error-boundary";
import { kinds } from "nostr-tools";
function GoalsBrowsePage() {
const { filter, listId } = usePeopleListContext();
@ -29,15 +29,13 @@ function GoalsBrowsePage() {
},
[showClosed.isOpen],
);
const timeline = useTimelineLoader(
const { loader, timeline: goals } = useTimelineLoader(
`${listId}-browse-goals`,
readRelays,
filter ? { ...filter, kinds: [GOAL_KIND] } : undefined,
filter ? { ...filter, kinds: [kinds.ZapGoal] } : undefined,
{ eventFilter },
);
const goals = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
@ -50,7 +48,7 @@ function GoalsBrowsePage() {
</Flex>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{goals.map((event) => (
{goals?.map((event) => (
<ErrorBoundary key={getEventUID(event)} event={event}>
<GoalCard goal={event} />
</ErrorBoundary>

View File

@ -1,33 +1,30 @@
import { Button, Center, Divider, Flex, Heading, Link, SimpleGrid, Spacer } from "@chakra-ui/react";
import { Navigate, Link as RouterLink } from "react-router-dom";
import { getEventUID } from "applesauce-core/helpers";
import { kinds } from "nostr-tools";
import useCurrentAccount from "../../hooks/use-current-account";
import { ExternalLinkIcon } from "../../components/icons";
import { getEventUID } from "../../helpers/nostr/event";
import { useReadRelays } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import GoalCard from "./components/goal-card";
import { GOAL_KIND } from "../../helpers/nostr/goal";
import VerticalPageLayout from "../../components/vertical-page-layout";
function UserGoalsManagerPage() {
const account = useCurrentAccount()!;
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: goals } = useTimelineLoader(
`${account.pubkey}-goals`,
readRelays,
account.pubkey
? {
authors: [account.pubkey],
kinds: [GOAL_KIND],
kinds: [kinds.ZapGoal],
}
: undefined,
);
const goals = useSubject(timeline.timeline);
if (goals.length === 0) {
return (
<Center p="10" fontSize="lg" whiteSpace="pre">
@ -45,7 +42,7 @@ function UserGoalsManagerPage() {
return (
<>
{goals.length > 0 && (
{goals && goals.length > 0 && (
<>
<Heading size="md" mt="2">
Created goals

View File

@ -67,14 +67,14 @@ function HashTagPage() {
},
[showReplies.isOpen, showReposts.isOpen, muteFilter, timelinePageEventFilter],
);
const timeline = useTimelineLoader(
const { loader, timeline } = useTimelineLoader(
`${listId ?? "global"}-${hashtag}-hashtag`,
readRelays,
{ kinds: [1], "#t": [hashtag], ...filter },
{ eventFilter },
);
useRelaysChanged(readRelays, () => timeline.reset());
useRelaysChanged(readRelays, () => loader.reset());
const header = (
<Flex gap="2" alignItems="center" wrap="wrap">
@ -103,7 +103,7 @@ function HashTagPage() {
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="2" pb="12" px="2" />;
return <TimelinePage loader={loader} timeline={timeline} header={header} pt="2" pb="12" px="2" />;
}
export default function HashTagView() {

View File

@ -42,9 +42,14 @@ function HomePage() {
const { listId, filter } = usePeopleListContext();
const { kinds } = useKindSelectionContext();
const timeline = useTimelineLoader(`${listId}-home-feed`, relays, filter ? { ...filter, kinds } : undefined, {
eventFilter,
});
const { loader, timeline } = useTimelineLoader(
`${listId}-home-feed`,
relays,
filter ? { ...filter, kinds } : undefined,
{
eventFilter,
},
);
const header = (
<Flex gap="2" wrap="wrap" alignItems="center">
@ -55,7 +60,7 @@ function HomePage() {
</Flex>
);
return <TimelinePage timeline={timeline} header={header} pt="2" pb="12" px="2" />;
return <TimelinePage loader={loader} timeline={timeline} header={header} pt="2" pb="12" px="2" />;
}
export default function HomeView() {

View File

@ -1,12 +1,12 @@
import { useMemo, useState } from "react";
import { Button, Card, CardBody, CardHeader, CardProps, Flex, Heading, Link, LinkBox, Text } from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { useObservable } from "applesauce-react";
import { nip19 } from "nostr-tools";
import KeyboardShortcut from "../../../components/keyboard-shortcut";
import useCurrentAccount from "../../../hooks/use-current-account";
import { useDMTimeline } from "../../../providers/global/dms-provider";
import useSubject from "../../../hooks/use-subject";
import {
KnownConversation,
groupIntoConversations,
@ -54,7 +54,7 @@ export default function DMsCard({ ...props }: Omit<CardProps, "children">) {
const timeline = useDMTimeline();
const messages = useSubject(timeline.timeline);
const messages = useObservable(timeline.timeline) ?? [];
const conversations = useMemo(() => {
const grouped = groupIntoConversations(messages)
.map((c) => identifyConversation(c, account.pubkey))

View File

@ -8,7 +8,6 @@ import { useReadRelays } from "../../../hooks/use-client-relays";
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import PeopleListProvider, { usePeopleListContext } from "../../../providers/local/people-list-provider";
import useSubject from "../../../hooks/use-subject";
import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
@ -52,9 +51,9 @@ function StreamsCardContent({ ...props }: Omit<CardProps, "children">) {
];
}, [filter]);
const timeline = useTimelineLoader(`${listId ?? "global"}-streams`, relays, query, { eventFilter });
const { loader, timeline } = useTimelineLoader(`${listId ?? "global"}-streams`, relays, query, { eventFilter });
const streams = useSubject(timeline.timeline)
const streams = timeline
.map((event) => {
try {
return parseStreamEvent(event);
@ -75,7 +74,7 @@ function StreamsCardContent({ ...props }: Omit<CardProps, "children">) {
<KeyboardShortcut letter="l" requireMeta ml="auto" onPress={() => navigate("/streams")} />
</CardHeader>
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
{streams.map((stream) => (
{streams?.map((stream) => (
<ErrorBoundary key={getEventUID(stream.event)} event={stream.event}>
<LiveStream stream={stream} />
</ErrorBoundary>

View File

@ -16,7 +16,6 @@ import { useCallback, useState } from "react";
import { NostrEvent } from "../../types/nostr-event";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import ListCard from "./components/list-card";
import { getEventUID } from "../../helpers/nostr/event";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -43,15 +42,13 @@ function BrowseListPage() {
[showEmpty.isOpen, showMute.isOpen, listKind],
);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: lists } = useTimelineLoader(
`${listId}-lists`,
readRelays,
filter ? { ...filter, kinds: [PEOPLE_LIST_KIND, NOTE_LIST_KIND] } : undefined,
{ eventFilter },
);
const lists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
@ -71,9 +68,7 @@ function BrowseListPage() {
</Flex>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{lists.map((event) => (
<ListCard key={getEventUID(event)} list={event} />
))}
{lists?.map((event) => <ListCard key={getEventUID(event)} list={event} />)}
</SimpleGrid>
</VerticalPageLayout>
</IntersectionObserverProvider>

View File

@ -2,13 +2,13 @@ import { useCallback, useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button, Flex } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { useObservable } from "applesauce-react";
import ngeohash from "ngeohash";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import "leaflet.locatecontrol/dist/L.Control.Locate.min.css";
import "leaflet.locatecontrol";
import useSubject from "../../hooks/use-subject";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
@ -56,7 +56,7 @@ export default function MapView() {
const [cells, setCells] = useState<string[]>([]);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline } = useTimelineLoader(
"geo-events",
readRelays,
cells.length > 0 ? { "#g": cells, kinds: [kinds.ShortTextNote] } : undefined,
@ -82,7 +82,7 @@ export default function MapView() {
setFocused(event.id);
}, []);
const events = useSubject(timeline.timeline);
const events = useObservable(loader.timeline) ?? [];
useEventMarkers(events, map, handleMarkerClick);
return (
@ -98,8 +98,8 @@ export default function MapView() {
</Flex>
<Flex overflowY="auto" overflowX="hidden" gap="2" direction="column" h="full">
<MapTimeline timeline={timeline} focused={focused} />
{cells.length > 0 && <TimelineActionAndStatus timeline={timeline} />}
<MapTimeline timeline={loader} focused={focused} />
{cells.length > 0 && <TimelineActionAndStatus timeline={loader} />}
</Flex>
</Flex>

View File

@ -2,18 +2,17 @@ import React from "react";
import { kinds } from "nostr-tools";
import { ErrorBoundary } from "../../components/error-boundary";
import useSubject from "../../hooks/use-subject";
import StreamNote from "../../components/timeline-page/generic-note-timeline/stream-note";
import { STREAM_KIND } from "../../helpers/nostr/stream";
import TimelineLoader from "../../classes/timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import TimelineNote from "../../components/note/timeline-note";
import { useObservable } from "applesauce-react";
const RenderEvent = React.memo(({ event, focused }: { event: NostrEvent; focused?: boolean }) => {
switch (event.kind) {
case kinds.ShortTextNote:
return <TimelineNote event={event} variant={focused ? "elevated" : undefined} />;
case STREAM_KIND:
case kinds.LiveEvent:
return <StreamNote event={event} />;
default:
return null;
@ -21,11 +20,11 @@ const RenderEvent = React.memo(({ event, focused }: { event: NostrEvent; focused
});
const MapTimeline = React.memo(({ timeline, focused }: { timeline: TimelineLoader; focused?: string }) => {
const events = useSubject(timeline.timeline);
const events = useObservable(timeline.timeline);
return (
<>
{events.map((event) => (
{events?.map((event) => (
<ErrorBoundary key={event.id} event={event}>
<RenderEvent event={event} focused={focused === event.id} />
</ErrorBoundary>

View File

@ -1,5 +1,6 @@
import { MouseEventHandler, useCallback, useMemo } from "react";
import { kinds } from "nostr-tools";
import { kinds, NostrEvent } from "nostr-tools";
import { useObservable } from "applesauce-react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import useCurrentAccount from "../../hooks/use-current-account";
@ -11,7 +12,6 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import { useNotifications } from "../../providers/global/notifications-provider";
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
import { groupByRoot } from "../../helpers/notification";
import { NostrEvent } from "../../types/nostr-event";
import { ChevronLeftIcon } from "../../components/icons";
import { AvatarGroup, Box, Button, ButtonGroup, Flex, LinkBox, Text, useDisclosure } from "@chakra-ui/react";
import UserAvatarLink from "../../components/user/user-avatar-link";
@ -67,7 +67,7 @@ function ThreadGroup({ rootId, events }: { rootId: string; events: NostrEvent[]
const ref = useEventIntersectionRef(events[events.length - 1]);
return (
<Flex>
<Flex ref={ref}>
<GitBranch01 boxSize={8} color="green.500" mr="2" />
<Flex direction="column" gap="2">
<AvatarGroup size="sm">
@ -104,7 +104,7 @@ function ThreadsNotificationsPage() {
const { timeline } = useNotifications();
const callback = useTimelineCurserIntersectionCallback(timeline);
const events = useSubject(timeline?.timeline);
const events = useObservable(timeline?.timeline) ?? [];
const filteredEvents = useMemo(
() =>

View File

@ -16,7 +16,7 @@ export default function SelectRelaySet({
pubkey?: string;
}) {
const account = useCurrentAccount();
const relaySets = useUserRelaySets(pubkey || account?.pubkey);
const relaySets = useUserRelaySets(pubkey || account?.pubkey) ?? [];
return (
<Select

View File

@ -7,7 +7,6 @@ import PeopleListProvider, { usePeopleListContext } from "../../providers/local/
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { NostrEvent } from "../../types/nostr-event";
import { getListName, getRelaysFromList } from "../../helpers/nostr/lists";
@ -36,12 +35,15 @@ function RelaySetCard({ set }: { set: NostrEvent }) {
function BrowseRelaySetsPage() {
const relays = useReadRelays();
const { filter } = usePeopleListContext();
const timeline = useTimelineLoader("relay-sets", relays, filter && { kinds: [kinds.Relaysets], ...filter }, {
eventFilter: (e) => getRelaysFromList(e).length > 0,
});
const relaySets = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const { loader, timeline: relaySets } = useTimelineLoader(
"relay-sets",
relays,
filter && { kinds: [kinds.Relaysets], ...filter },
{
eventFilter: (e) => getRelaysFromList(e).length > 0,
},
);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -49,11 +51,9 @@ function BrowseRelaySetsPage() {
<PeopleListSelection />
</Flex>
<IntersectionObserverProvider callback={callback}>
{relaySets.map((set) => (
<RelaySetCard key={getEventUID(set)} set={set} />
))}
{relaySets?.map((set) => <RelaySetCard key={getEventUID(set)} set={set} />)}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</VerticalPageLayout>
);
}

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Button, ButtonGroup, Card, Flex, Heading, Text } from "@chakra-ui/react";
import { Button, ButtonGroup, Card, Flex, Heading, Text, useForceUpdate } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { localRelay } from "../../../../services/local-relay";
@ -17,40 +17,33 @@ async function importEvents(events: NostrEvent[]) {
}
async function exportEvents() {
if (localRelay instanceof MemoryRelay) {
return localRelay.events.getSortedEvents();
return Array.from(localRelay.store.database.iterateTime(0, Infinity));
}
return [];
}
export default function MemoryDatabasePage() {
const [update, setUpdate] = useState(0);
const update = useForceUpdate();
useEffect(() => {
if (localRelay instanceof MemoryRelay) {
const sub = localRelay.events.onEvent.subscribe((e) => setUpdate((v) => v + 1));
const sub = localRelay.store.database.inserted.subscribe(update);
return () => sub.unsubscribe();
}
}, []);
const count = useMemo(() => {
if (localRelay instanceof MemoryRelay) return localRelay.events.events.size;
if (localRelay instanceof MemoryRelay) return localRelay.store.database.events.size;
return 0;
}, [update]);
const kinds = useMemo(() => {
if (localRelay instanceof MemoryRelay) {
return getSortedKinds(Array.from(localRelay.events.events.values()));
return getSortedKinds(Array.from(localRelay.store.database.iterateTime(0, Infinity)));
}
return {};
}, [update]);
const handleClearData = async () => {
if (localRelay instanceof MemoryRelay) {
localRelay.events.clear();
setUpdate(-1);
}
};
return (
<>
<Text>Total events: {count ?? "Loading..."}</Text>
@ -58,11 +51,6 @@ export default function MemoryDatabasePage() {
<ImportEventsButton onLoad={importEvents} />
<ExportEventsButton getEvents={exportEvents} />
</ButtonGroup>
<ButtonGroup flexWrap="wrap">
<Button onClick={handleClearData} colorScheme="primary" variant="outline">
Clear cache
</Button>
</ButtonGroup>
<Flex gap="2" wrap="wrap" alignItems="flex-start" w="full">
{kinds && (
<>

View File

@ -82,7 +82,7 @@ function NipTag({ nip, name }: { nip: number; name?: boolean }) {
return (
<Tooltip label={NIP_NAMES[nipStr]}>
<Tag as="a" target="_blank" href={`https://github.com/nostr-protocol/nips/blob/master/${nipStr}.md`}>
{name ? NIP_NAMES[nipStr] ?? nipNumber : nipNumber}
{name ? (NIP_NAMES[nipStr] ?? nipNumber) : nipNumber}
</Tag>
</Tooltip>
);

View File

@ -12,6 +12,7 @@ import PeopleListSelection from "../../../components/people-list-selection/peopl
import { usePeopleListContext } from "../../../providers/local/people-list-provider";
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
import NoteFilterTypeButtons from "../../../components/note-filter-type-buttons";
import { getSeenRelays } from "applesauce-core/helpers";
export default function RelayNotes({ relay }: { relay: string }) {
useAppTitle(`${relay} - Notes`);
@ -25,6 +26,7 @@ export default function RelayNotes({ relay }: { relay: string }) {
const muteFilter = useClientSideMuteFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (!getSeenRelays(event)?.has(relay)) return false;
if (muteFilter(event)) return false;
if (!showReplies.isOpen && isReply(event)) return false;
if (!showReposts.isOpen && isRepost(event)) return false;
@ -32,10 +34,15 @@ export default function RelayNotes({ relay }: { relay: string }) {
},
[timelineEventFilter, showReplies.isOpen, showReposts.isOpen, muteFilter],
);
const timeline = useTimelineLoader(`${relay}-notes`, [relay], filter ? { ...filter, kinds: k } : undefined, {
eventFilter,
useCache: false,
});
const { loader, timeline } = useTimelineLoader(
`${relay}-notes`,
[relay],
filter ? { ...filter, kinds: k } : undefined,
{
eventFilter,
useCache: false,
},
);
const header = (
<Flex gap="2" wrap="wrap" px={["2", 0]}>
@ -46,5 +53,5 @@ export default function RelayNotes({ relay }: { relay: string }) {
</Flex>
);
return <TimelinePage timeline={timeline} header={header} />;
return <TimelinePage loader={loader} timeline={timeline} header={header} />;
}

View File

@ -2,18 +2,19 @@ import { Flex } from "@chakra-ui/react";
import { RELAY_REVIEW_LABEL } from "../../../helpers/nostr/reviews";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useSubject from "../../../hooks/use-subject";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import RelayReviewNote from "../components/relay-review-note";
import { useAppTitle } from "../../../hooks/use-app-title";
import { usePeopleListContext } from "../../../providers/local/people-list-provider";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
export default function RelayReviews({ relay }: { relay: string }) {
useAppTitle(`${relay} - Reviews`);
const readRelays = useReadRelays();
const { filter } = usePeopleListContext();
const timeline = useTimelineLoader(
const { loader, timeline: reviews } = useTimelineLoader(
`${relay}-reviews`,
readRelays,
filter
@ -25,14 +26,13 @@ export default function RelayReviews({ relay }: { relay: string }) {
}
: undefined,
);
const events = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<Flex direction="column" gap="2">
{events.map((event) => (
<RelayReviewNote key={event.id} event={event} hideUrl />
))}
<IntersectionObserverProvider callback={callback}>
{reviews?.map((event) => <RelayReviewNote key={event.id} event={event} hideUrl />)}
</IntersectionObserverProvider>
</Flex>
);
}

View File

@ -7,7 +7,6 @@ import useTimelineLoader from "../../../hooks/use-timeline-loader";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { usePeopleListContext } from "../../../providers/local/people-list-provider";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status";
import UserAvatarLink from "../../../components/user/user-avatar-link";
@ -36,7 +35,7 @@ function UserCard({ list, pubkey }: { list: NostrEvent; pubkey: string }) {
export default function RelayUsersTab({ relay }: { relay: string }) {
useAppTitle(`${relay} - Users`);
const { filter } = usePeopleListContext();
const timeline = useTimelineLoader(
const { loader, timeline: lists } = useTimelineLoader(
`${relay}-users`,
[relay],
filter && { ...filter, kinds: [kinds.RelayList], "#r": getRelayVariations(relay) },
@ -44,9 +43,7 @@ export default function RelayUsersTab({ relay }: { relay: string }) {
eventFilter: (e) => getRelaysFromList(e).includes(relay),
},
);
const lists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<Flex direction="column" gap="2">
@ -55,12 +52,10 @@ export default function RelayUsersTab({ relay }: { relay: string }) {
</Flex>
<IntersectionObserverProvider callback={callback}>
<SimpleGrid columns={[1, 1, 2, 3, 4]} spacing="2">
{lists.map((list) => (
<UserCard key={getEventUID(list)} pubkey={list.pubkey} list={list} />
))}
{lists?.map((list) => <UserCard key={getEventUID(list)} pubkey={list.pubkey} list={list} />)}
</SimpleGrid>
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
);
}

View File

@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
import { useReadRelays } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import RelayReviewNote from "./components/relay-review-note";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
@ -17,7 +16,7 @@ function RelayReviewsPage() {
const readRelays = useReadRelays();
const { filter } = usePeopleListContext();
const timeline = useTimelineLoader(
const { loader, timeline: reviews } = useTimelineLoader(
"relay-reviews",
readRelays,
filter
@ -28,10 +27,7 @@ function RelayReviewsPage() {
}
: undefined,
);
const reviews = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
@ -43,9 +39,7 @@ function RelayReviewsPage() {
<PeopleListSelection />
<Heading size="md">Relay Reviews</Heading>
</Flex>
{reviews.map((event) => (
<RelayReviewNote key={event.id} event={event} />
))}
{reviews?.map((event) => <RelayReviewNote key={event.id} event={event} />)}
</VerticalPageLayout>
</IntersectionObserverProvider>
);

View File

@ -8,13 +8,11 @@ import { LightningIcon } from "../../../components/icons";
import { readablizeSats } from "../../../helpers/bolt11";
import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline";
import { ParsedStream } from "../../../helpers/nostr/stream";
import useSubject from "../../../hooks/use-subject";
import UserAvatarLink from "../../../components/user/user-avatar-link";
export default function TopZappers({ stream, ...props }: FlexProps & { stream: ParsedStream }) {
const timeline = useStreamChatTimeline(stream);
const events = useSubject(timeline.timeline);
const zaps = useMemo(() => parseZapEvents(events.filter((e) => e.kind === kinds.Zap)), [events]);
const { timeline } = useStreamChatTimeline(stream);
const zaps = useMemo(() => parseZapEvents(timeline.filter((e) => e.kind === kinds.Zap)), [timeline]);
const totals: Record<string, number> = {};
for (const zap of zaps) {

View File

@ -9,10 +9,10 @@ import ChatMessageForm from "../stream/stream-chat/stream-chat-form";
import { ParsedStream } from "../../../helpers/nostr/stream";
function ChatCard({ stream }: { stream: ParsedStream }) {
const timeline = useStreamChatTimeline(stream);
const { loader } = useStreamChatTimeline(stream);
const scrollBox = useRef<HTMLDivElement | null>(null);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<Flex flex={1} direction="column" overflow="hidden" p={0}>

View File

@ -7,7 +7,6 @@ import "./styles.css";
import "react-mosaic-component/react-mosaic-component.css";
import useParsedStreams from "../../../hooks/use-parsed-streams";
import useSubject from "../../../hooks/use-subject";
import { ParsedStream, STREAM_KIND, getATag } from "../../../helpers/nostr/stream";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import RequireCurrentAccount from "../../../providers/route/require-current-account";
@ -72,7 +71,7 @@ function StreamModerationPage() {
const account = useCurrentAccount()!;
const readRelays = useReadRelays();
const timeline = useTimelineLoader(account.pubkey + "-streams", readRelays, [
const { loader, timeline } = useTimelineLoader(account.pubkey + "-streams", readRelays, [
{
authors: [account.pubkey],
kinds: [STREAM_KIND],
@ -80,8 +79,7 @@ function StreamModerationPage() {
{ "#p": [account.pubkey], kinds: [STREAM_KIND] },
]);
const streamEvents = useSubject(timeline.timeline);
const streams = useParsedStreams(streamEvents);
const streams = useParsedStreams(timeline);
const [selected, setSelected] = useState<ParsedStream>();

View File

@ -1,7 +1,7 @@
import { ReactNode, memo, useMemo, useState } from "react";
import { useInterval } from "react-use";
import { Button, ButtonGroup, Divider, Flex, Heading } from "@chakra-ui/react";
import dayjs from "dayjs";
import { useInterval, useObservable } from "react-use";
import useCurrentAccount from "../../../hooks/use-current-account";
import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline";
@ -57,11 +57,7 @@ function UserCard({ pubkey }: { pubkey: string }) {
function UsersCard({ stream }: { stream: ParsedStream }) {
const account = useCurrentAccount()!;
const streamChatTimeline = useStreamChatTimeline(stream);
// refresh when a new event
useObservable(streamChatTimeline.events.onEvent);
const chatEvents = streamChatTimeline.events.getSortedEvents();
const { loader, timeline: chatEvents } = useStreamChatTimeline(stream);
const muteList = useUserMuteList(account.pubkey);
const pubkeysInChat = useMemo(() => {

View File

@ -1,18 +1,16 @@
import { memo } from "react";
import { Flex } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { useObservable } from "react-use";
import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline";
import ZapMessageMemo from "../stream/stream-chat/zap-message";
import { ParsedStream } from "../../../helpers/nostr/stream";
function ZapsCard({ stream }: { stream: ParsedStream }) {
const streamChatTimeline = useStreamChatTimeline(stream);
const { timeline } = useStreamChatTimeline(stream);
// refresh when a new event
useObservable(streamChatTimeline.events.onEvent);
const zapMessages = streamChatTimeline.events.getSortedEvents().filter((event) => {
const zapMessages = timeline.filter((event) => {
if (stream.starts && event.created_at < stream.starts) return false;
if (stream.ends && event.created_at > stream.ends) return false;
if (event.kind !== kinds.Zap) return false;
@ -21,9 +19,7 @@ function ZapsCard({ stream }: { stream: ParsedStream }) {
return (
<Flex flex={1} p="2" gap="2" overflowY="auto" overflowX="hidden" flexDirection="column">
{zapMessages.map((event) => (
<ZapMessageMemo key={event.id} zap={event} stream={stream} />
))}
{zapMessages?.map((event) => <ZapMessageMemo key={event.id} zap={event} stream={stream} />)}
</Flex>
);
}

View File

@ -1,13 +1,11 @@
import { useCallback, useMemo } from "react";
import { Flex, Heading, SimpleGrid, Switch } from "@chakra-ui/react";
import { Filter } from "nostr-tools";
import { Filter, kinds } from "nostr-tools";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import StreamCard from "./components/stream-card";
import { STREAM_KIND } from "../../helpers/nostr/stream";
import useRelaysChanged from "../../hooks/use-relays-changed";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
@ -39,19 +37,17 @@ function StreamsPage() {
const query = useMemo<Filter | Filter[] | undefined>(() => {
if (!filter) return undefined;
return [
{ authors: filter.authors, kinds: [STREAM_KIND] },
{ "#p": filter.authors, kinds: [STREAM_KIND] },
{ authors: filter.authors, kinds: [kinds.LiveEvent] },
{ "#p": filter.authors, kinds: [kinds.LiveEvent] },
];
}, [filter]);
const timeline = useTimelineLoader(`${listId ?? "global"}-streams`, relays, query, { eventFilter });
const { loader, timeline } = useTimelineLoader(`${listId ?? "global"}-streams`, relays, query, { eventFilter });
const callback = useTimelineCurserIntersectionCallback(loader);
useRelaysChanged(relays, () => timeline.reset());
useRelaysChanged(relays, () => loader.reset());
const callback = useTimelineCurserIntersectionCallback(timeline);
const events = useSubject(timeline.timeline);
const streams = useParsedStreams(events);
const streams = useParsedStreams(timeline);
const liveStreams = streams.filter((stream) => stream.status === "live");
const endedStreams = streams.filter((stream) => stream.status === "ended");
@ -85,7 +81,7 @@ function StreamsPage() {
</SimpleGrid>
</>
)}
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</IntersectionObserverProvider>
</VerticalPageLayout>
);

View File

@ -1,9 +1,9 @@
import { forwardRef } from "react";
import { Flex, FlexProps } from "@chakra-ui/react";
import { css } from "@emotion/react";
import { kinds } from "nostr-tools";
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND } from "../../../../helpers/nostr/stream";
import useSubject from "../../../../hooks/use-subject";
import { ParsedStream } from "../../../../helpers/nostr/stream";
import useStreamChatTimeline from "./use-stream-chat-timeline";
import ChatMessage from "./chat-message";
import ZapMessage from "./zap-message";
@ -20,8 +20,7 @@ const StreamChatLog = forwardRef<
HTMLDivElement,
Omit<FlexProps, "children"> & { stream: ParsedStream; hideScrollbar?: boolean }
>(({ stream, hideScrollbar, ...props }, ref) => {
const timeline = useStreamChatTimeline(stream);
const events = useSubject(timeline.timeline);
const { timeline: events } = useStreamChatTimeline(stream);
return (
<Flex
@ -34,7 +33,7 @@ const StreamChatLog = forwardRef<
{...props}
>
{events.map((event) =>
event.kind === STREAM_CHAT_MESSAGE_KIND ? (
event.kind === kinds.LiveChatMessage ? (
<ChatMessage key={event.id} event={event} stream={stream} />
) : (
<ZapMessage key={event.id} zap={event} stream={stream} />

View File

@ -18,10 +18,10 @@ export default function StreamChat({
displayMode,
...props
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
const timeline = useStreamChatTimeline(stream);
const { loader } = useStreamChatTimeline(stream);
const scrollBox = useRef<HTMLDivElement | null>(null);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
const isPopup = !!displayMode;
const isChatLog = displayMode === "log";

View File

@ -12,7 +12,6 @@ import PostRepostsTab from "./tabs/reposts";
import PostQuotesTab from "./tabs/quotes";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import useSubject from "../../../hooks/use-subject";
import { getContentTagRefs } from "../../../helpers/nostr/event";
import { CORRECTION_EVENT_KIND } from "../../../helpers/nostr/corrections";
import CorrectionsTab from "./tabs/corrections";
@ -34,8 +33,9 @@ export default function DetailsTabs({ post }: { post: ThreadItem }) {
const zaps = useEventZaps(getEventUID(post.event));
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${post.event.id}-thread-refs`, readRelays, { "#e": [post.event.id] });
const events = useSubject(timeline.timeline);
const { loader, timeline: events } = useTimelineLoader(`${post.event.id}-thread-refs`, readRelays, {
"#e": [post.event.id],
});
const reactions = events.filter((e) => e.kind === kinds.Reaction);
const reposts = events.filter((e) => e.kind === kinds.Repost || e.kind === kinds.GenericRepost);

View File

@ -2,24 +2,24 @@ import { Flex, Heading } from "@chakra-ui/react";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useSubject from "../../../hooks/use-subject";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import PeopleListProvider, { usePeopleListContext } from "../../../providers/local/people-list-provider";
import BackButton from "../../../components/router/back-button";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import CorrectionCard from "./correction-card";
import { CORRECTION_EVENT_KIND } from "../../../helpers/nostr/corrections";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../../providers/local/intersection-observer";
function CorrectionsPage() {
const { listId, filter } = usePeopleListContext();
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: corrections } = useTimelineLoader(
`${listId}-corrections`,
readRelays,
filter ? [{ kinds: [CORRECTION_EVENT_KIND], ...filter }] : undefined,
);
const corrections = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -29,9 +29,9 @@ function CorrectionsPage() {
<PeopleListSelection />
</Flex>
{corrections.map((correction) => (
<CorrectionCard correction={correction} key={correction.id} />
))}
<IntersectionObserverProvider callback={callback}>
{corrections?.map((correction) => <CorrectionCard correction={correction} key={correction.id} />)}
</IntersectionObserverProvider>
</VerticalPageLayout>
);
}

View File

@ -10,7 +10,6 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import useSubject from "../../hooks/use-subject";
import EmbeddedDM from "../../components/embed-event/event-types/embedded-dm";
import { NostrEvent } from "../../types/nostr-event";
import { ChevronLeftIcon } from "../../components/icons";
@ -41,7 +40,7 @@ export function DMTimelinePage() {
[clientMuteFilter],
);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(
const { loader, timeline: dms } = useTimelineLoader(
`${listId ?? "global"}-dm-feed`,
readRelays,
filter
@ -55,9 +54,7 @@ export function DMTimelinePage() {
: { kinds: [kinds.EncryptedDirectMessage] },
{ eventFilter },
);
const dms = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -68,7 +65,7 @@ export function DMTimelinePage() {
<PeopleListSelection />
</Flex>
<IntersectionObserverProvider callback={callback}>
{dms.map((dm) => (
{dms?.map((dm) => (
<ErrorBoundary key={dm.id} event={dm}>
<DirectMessage dm={dm} />
</ErrorBoundary>

View File

@ -13,7 +13,6 @@ import {
DVM_TTS_RESULT_KIND,
groupEventsIntoJobs,
} from "../../../../helpers/nostr/dvm";
import useSubject from "../../../../hooks/use-subject";
import { DraftNostrEvent, NostrEvent } from "../../../../types/nostr-event";
import relayScoreboardService from "../../../../services/relay-scoreboard";
import useCurrentAccount from "../../../../hooks/use-current-account";
@ -42,7 +41,7 @@ export default function NoteTextToSpeechPage({ note }: { note: NostrEvent }) {
await publish("Request Reading", draft);
}, [publish, note, readRelays, lang]);
const timeline = useTimelineLoader(
const { loader, timeline: events } = useTimelineLoader(
`${getEventUID(note)}-readings`,
readRelays,
[
@ -54,7 +53,6 @@ export default function NoteTextToSpeechPage({ note }: { note: NostrEvent }) {
].filter(Boolean) as Filter[],
);
const events = useSubject(timeline.timeline);
const jobs = groupEventsIntoJobs(events);
return (

View File

@ -13,12 +13,12 @@ import {
} from "@chakra-ui/react";
import dayjs from "dayjs";
import codes from "iso-language-codes";
import { Filter } from "nostr-tools";
import { getEventUID } from "applesauce-core/helpers";
import { DraftNostrEvent, NostrEvent } from "../../../../types/nostr-event";
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { getEventUID } from "../../../../helpers/nostr/event";
import { useReadRelays } from "../../../../hooks/use-client-relays";
import useSubject from "../../../../hooks/use-subject";
import relayScoreboardService from "../../../../services/relay-scoreboard";
import {
DVM_STATUS_KIND,
@ -29,7 +29,6 @@ import {
import useCurrentAccount from "../../../../hooks/use-current-account";
import TranslationJob from "./translation-job";
import { usePublishEvent } from "../../../../providers/global/publish-provider";
import { Filter } from "nostr-tools";
export function NoteTranslationsPage({ note }: { note: NostrEvent }) {
const account = useCurrentAccount();
@ -54,7 +53,7 @@ export function NoteTranslationsPage({ note }: { note: NostrEvent }) {
await publish("Request Translation", draft);
}, [publish, note, readRelays, lang]);
const timeline = useTimelineLoader(
const { loader, timeline: events } = useTimelineLoader(
`${getEventUID(note)}-translations`,
readRelays,
[
@ -66,7 +65,6 @@ export function NoteTranslationsPage({ note }: { note: NostrEvent }) {
].filter(Boolean) as Filter[],
);
const events = useSubject(timeline.timeline);
const jobs = Object.values(groupEventsIntoJobs(events));
return (

View File

@ -10,12 +10,10 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useReadRelays } from "../../hooks/use-client-relays";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";
import { ChevronLeftIcon } from "../../components/icons";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import { EmbedEvent } from "../../components/embed-event";
import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream";
import {
BOOKMARK_LIST_KIND,
BOOKMARK_LIST_SET_KIND,
@ -44,12 +42,12 @@ const commonTimelineKinds = [
kinds.Reaction,
kinds.BadgeAward,
kinds.BadgeDefinition,
STREAM_KIND,
kinds.LiveEvent,
kinds.Contacts,
kinds.Metadata,
kinds.EncryptedDirectMessage,
MUTE_LIST_KIND,
STREAM_CHAT_MESSAGE_KIND,
kinds.LiveChatMessage,
kinds.EventDeletion,
kinds.CommunityPostApproval,
BOOKMARK_LIST_KIND,
@ -57,6 +55,7 @@ const commonTimelineKinds = [
PEOPLE_LIST_KIND,
PIN_LIST_KIND,
COMMUNITIES_LIST_KIND,
kinds.ZapGoal,
];
export function UnknownTimelinePage() {
@ -73,10 +72,10 @@ export function UnknownTimelinePage() {
[clientMuteFilter],
);
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${listId ?? "global"}-unknown-feed`, readRelays, filter, { eventFilter });
const events = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const { loader, timeline } = useTimelineLoader(`${listId ?? "global"}-unknown-feed`, readRelays, filter, {
eventFilter,
});
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -87,8 +86,8 @@ export function UnknownTimelinePage() {
<PeopleListSelection />
</Flex>
<IntersectionObserverProvider callback={callback}>
{events.map((dm) => (
<UnknownEvent key={dm.id} event={dm} />
{timeline.map((event) => (
<UnknownEvent key={event.id} event={event} />
))}
</IntersectionObserverProvider>
</VerticalPageLayout>

View File

@ -9,7 +9,6 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import { NostrEvent } from "../../types/nostr-event";
import { TORRENT_KIND, validateTorrent } from "../../helpers/nostr/torrents";
import useSubject from "../../hooks/use-subject";
import TorrentTableRow from "./components/torrent-table-row";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
@ -82,12 +81,10 @@ function TorrentsPage() {
if (tags.length > 0) return { ...filter, kinds: [TORRENT_KIND], "#t": tags };
else return { ...filter, kinds: [TORRENT_KIND] };
}, [tags.join(","), filter]);
const timeline = useTimelineLoader(`${listId || "global"}-torrents`, relays, query, {
const { loader, timeline: torrents } = useTimelineLoader(`${listId || "global"}-torrents`, relays, query, {
eventFilter,
});
const torrents = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
const account = useCurrentAccount();
@ -115,11 +112,7 @@ function TorrentsPage() {
<Th />
</Tr>
</Thead>
<Tbody>
{torrents.map((torrent) => (
<TorrentTableRow key={torrent.id} torrent={torrent} />
))}
</Tbody>
<Tbody>{torrents?.map((torrent) => <TorrentTableRow key={torrent.id} torrent={torrent} />)}</Tbody>
</Table>
</TableContainer>
</IntersectionObserverProvider>

View File

@ -3,7 +3,6 @@ import { Flex } from "@chakra-ui/react";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { STEMSTR_RELAY, STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr";
import useSubject from "../../hooks/use-subject";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
@ -27,12 +26,15 @@ function TracksPage() {
},
[clientMuteFilter],
);
const timeline = useTimelineLoader(`${listId}-tracks`, relays, filter && { kinds: [STEMSTR_TRACK_KIND], ...filter }, {
eventFilter,
});
const tracks = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const { loader, timeline: tracks } = useTimelineLoader(
`${listId}-tracks`,
relays,
filter && { kinds: [STEMSTR_TRACK_KIND], ...filter },
{
eventFilter,
},
);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<VerticalPageLayout>
@ -40,9 +42,7 @@ function TracksPage() {
<PeopleListSelection />
</Flex>
<IntersectionObserverProvider callback={callback}>
{tracks.map((track) => (
<TrackCard track={track} />
))}
{tracks?.map((track) => <TrackCard key={track.id} track={track} />)}
</IntersectionObserverProvider>
</VerticalPageLayout>
);

View File

@ -3,7 +3,6 @@ import { kinds } from "nostr-tools";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
@ -15,23 +14,21 @@ export default function UserArticlesTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useAdditionalRelayContext();
const timeline = useTimelineLoader(pubkey + "-articles", readRelays, {
const { loader, timeline: articles } = useTimelineLoader(pubkey + "-articles", readRelays, {
authors: [pubkey],
kinds: [kinds.LongFormArticle],
});
const articles = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<VerticalPageLayout>
{articles.map((article) => (
{articles?.map((article) => (
<ErrorBoundary key={article.id} event={article}>
<ArticleCard article={article} />
</ErrorBoundary>
))}
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</VerticalPageLayout>
</IntersectionObserverProvider>
);

View File

@ -4,7 +4,6 @@ import { useOutletContext } from "react-router-dom";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -62,24 +61,20 @@ export default function UserDMsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useAdditionalRelayContext();
const timeline = useTimelineLoader(pubkey + "-articles", readRelays, [
const { loader, timeline: dms } = useTimelineLoader(pubkey + "-articles", readRelays, [
{
authors: [pubkey],
kinds: [kinds.EncryptedDirectMessage],
},
{ "#p": [pubkey], kinds: [kinds.EncryptedDirectMessage] },
]);
const dms = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<VerticalPageLayout>
{dms.map((dm) => (
<DirectMessage key={dm.id} dm={dm} pubkey={pubkey} />
))}
<TimelineActionAndStatus timeline={timeline} />
{dms?.map((dm) => <DirectMessage key={dm.id} dm={dm} pubkey={pubkey} />)}
<TimelineActionAndStatus timeline={loader} />
</VerticalPageLayout>
</IntersectionObserverProvider>
);

View File

@ -1,9 +1,9 @@
import { useOutletContext } from "react-router-dom";
import { Heading, SimpleGrid } from "@chakra-ui/react";
import { useObservable } from "applesauce-react";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { getEventUID } from "../../helpers/nostr/event";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
@ -17,16 +17,15 @@ export default function UserEmojiPacksTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useAdditionalRelayContext();
const timeline = useTimelineLoader(pubkey + "-emoji-packs", readRelays, {
const { loader, timeline: packs } = useTimelineLoader(pubkey + "-emoji-packs", readRelays, {
authors: [pubkey],
kinds: [EMOJI_PACK_KIND],
});
const packs = useSubject(timeline.timeline);
const favoritePacks = useFavoriteEmojiPacks(pubkey);
const favorites = useReplaceableEvents(favoritePacks && getPackCordsFromFavorites(favoritePacks));
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>

View File

@ -5,7 +5,6 @@ import { Event, kinds } from "nostr-tools";
import { useReadRelays } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
@ -28,22 +27,20 @@ export default function UserFollowersTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useReadRelays();
const timeline = useTimelineLoader(`${pubkey}-followers`, readRelays, {
const { loader, timeline: events } = useTimelineLoader(`${pubkey}-followers`, readRelays, {
"#p": [pubkey],
kinds: [kinds.Contacts],
});
const lists = useSubject(timeline.timeline);
const followerEvents = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
const followers = useMemo(() => {
const dedupe = new Map<string, Event>();
for (const event of followerEvents) {
for (const event of events) {
dedupe.set(event.pubkey, event);
}
return Array.from(dedupe.values());
}, [followerEvents]);
}, [events]);
return (
<IntersectionObserverProvider callback={callback}>
@ -52,7 +49,7 @@ export default function UserFollowersTab() {
<FollowerItem key={event.pubkey} event={event} />
))}
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />
<TimelineActionAndStatus timeline={loader} />
</IntersectionObserverProvider>
);
}

View File

@ -1,9 +1,8 @@
import { useOutletContext } from "react-router-dom";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { SimpleGrid } from "@chakra-ui/react";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { getEventUID } from "../../helpers/nostr/event";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
@ -15,21 +14,17 @@ export default function UserGoalsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useAdditionalRelayContext();
const timeline = useTimelineLoader(pubkey + "-goals", readRelays, {
const { loader, timeline: goals } = useTimelineLoader(pubkey + "-goals", readRelays, {
authors: [pubkey],
kinds: [GOAL_KIND],
});
const goals = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<VerticalPageLayout>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
{goals.map((goal) => (
<GoalCard key={getEventUID(goal)} goal={goal} />
))}
{goals?.map((goal) => <GoalCard key={getEventUID(goal)} goal={goal} />)}
</SimpleGrid>
</VerticalPageLayout>
</IntersectionObserverProvider>

Some files were not shown because too many files have changed in this diff Show More