mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
load timelines from cache relay
remove process class
This commit is contained in:
parent
1167dbae54
commit
6f933ad3e1
100
pnpm-lock.yaml
generated
100
pnpm-lock.yaml
generated
@ -104,28 +104,28 @@ importers:
|
||||
version: 0.7.2
|
||||
applesauce-accounts:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-content:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-core:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-factory:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-loaders:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-net:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(typescript@5.7.3)
|
||||
applesauce-react:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-signers:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
version: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
bech32:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@ -2192,32 +2192,32 @@ packages:
|
||||
engines: {node: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
applesauce-accounts@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-yymm0dQgBJGgf3nlv1DC+AyxK+cvHLZczmRLyYW4k4MCNM7mO7qwnFGbMJJftDmVw5l8f9xBu0T/8r3IWsOxJw==}
|
||||
applesauce-accounts@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-PuxwOkGC1wZrfVy585JTE6Eo7vnu9uN5Dt7rEb7PC/NFntv1RFSNfKOrXlEqzltiIo6StErN2ypFz2Tvtj20Eg==}
|
||||
|
||||
applesauce-content@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-7TpIqocvhn5g4M/Biua7AP3pWo4YtDa1C21uci/ElkbuqMNhVT37ASwttGM8GPcfLTxQ7uBkpPjOBTXm74D8Rw==}
|
||||
applesauce-content@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-B7lNI/wtB1JZ20h6keFwMscP/ClVuyCC0e/fI4fhtv1RzPbr+GMrHD+liIJZnIqodptD/li3B8ixH41Ux20JlA==}
|
||||
|
||||
applesauce-core@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-BNXzw8ydbp4KiPfn7X5iCMaeaNlAW5tr3o6dWz9UwV7ItWieQLd17ySh0KPLdVbE+e42msBN7ByE7iNC0ItJOQ==}
|
||||
applesauce-core@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-0Io5Vu1tXYm0ddtaK1DyboxrUblaKhEfYs/TIe9WhcGjBcHBNJgyqoAlZ9YNJ6TN6VX6CmyVAq779KH3IWtWgQ==}
|
||||
|
||||
applesauce-core@0.10.0:
|
||||
resolution: {integrity: sha512-QMhUh4FIARcqY5soCB4Z8DIu+py0rYb28IgWT4gP9DLBGpDrY8lStXk7W1/46TLjEH97y0hbiXFK7kMCZ31oOQ==}
|
||||
|
||||
applesauce-factory@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-O4xMkwaOTX7t6JYaS78WTKzIRekCipCBnDeZpyYmLK+h4rQ+8aKd21VonV4hfh9A8dTeS28QNFsdDabbH+22Nw==}
|
||||
applesauce-factory@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-1jMAzucp8as7bIyOHKwCHGQnkVuOmlaxoDohMth6nVhXclLBUp7pXzHDIwfqQndWWMCMq5TcPQAfYmlVsFQgAQ==}
|
||||
|
||||
applesauce-loaders@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-ZWU23UtUQPQdM26NFLUralB8M8IuxrJqYqCwA5j5pozi5EG49OxxdPaZuXn3u37VRX8mibQYRVQqR+rhPWEC6g==}
|
||||
applesauce-loaders@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-ZhzJnnLLigJ/WGXtm3oeQSC3MwGpII92FmO0/Rbrd4vceOOx7b9hEbseDTtfYUtY8e/abahYuKqLH0d8QQFtiA==}
|
||||
|
||||
applesauce-net@0.10.0:
|
||||
resolution: {integrity: sha512-ZsAs/MkeGHiPZ2/a8lwP8lx/Eh+5Dot0qG4BLTAqjg4emP/RsiqW+hyc6v6QcVbdvuR0+hP1gka3+wWtiy/cTA==}
|
||||
|
||||
applesauce-react@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-/LloC27jD9BieXmoTZs59SVbHw/vzjh/mh1jt98TQT8NcSh9zoPD1quR4a4BNFXUmeDDAxOi4VcrTOCVXAvweA==}
|
||||
applesauce-react@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-KVizXnEkyjHJuVyl11oUArKAIk760U9olSH0hSSkhvPmvzmfUKFvNmugaIeyarr5FJA1qoebkrpsYzbIkvXnMA==}
|
||||
|
||||
applesauce-signers@0.0.0-next-20250203180810:
|
||||
resolution: {integrity: sha512-PJBPVmLW+lDYQ2TYflJGr/ddmvDu0v19l6I9XBhHfmlS1ypAej9kk4c4X56gRzROeq2zgIs/SSi1+Jm454AqRw==}
|
||||
applesauce-signers@0.0.0-next-20250203215539:
|
||||
resolution: {integrity: sha512-o2xftPZuBDLWobAGPdCxWkAGY8F7NqmS85Dra8sVjSB8ZqFOIjII+2iBcqKAk5ytU23FAH3TzZXt1/11w0dGgQ==}
|
||||
|
||||
arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
@ -5194,8 +5194,8 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.0:
|
||||
resolution: {integrity: sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==}
|
||||
semver@7.7.1:
|
||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -6887,7 +6887,7 @@ snapshots:
|
||||
plist: 3.1.0
|
||||
prompts: 2.4.2
|
||||
rimraf: 4.4.1
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
tar: 6.2.1
|
||||
tslib: 2.6.2
|
||||
xml2js: 0.5.0
|
||||
@ -6909,7 +6909,7 @@ snapshots:
|
||||
plist: 3.1.0
|
||||
prompts: 2.4.2
|
||||
rimraf: 4.4.1
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
tar: 6.2.1
|
||||
tslib: 2.8.1
|
||||
xml2js: 0.5.0
|
||||
@ -7161,7 +7161,7 @@ snapshots:
|
||||
outdent: 0.5.0
|
||||
prettier: 2.8.8
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
|
||||
'@changesets/assemble-release-plan@6.0.5':
|
||||
dependencies:
|
||||
@ -7170,7 +7170,7 @@ snapshots:
|
||||
'@changesets/should-skip-package': 0.1.1
|
||||
'@changesets/types': 6.0.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
|
||||
'@changesets/changelog-git@0.2.0':
|
||||
dependencies:
|
||||
@ -7203,7 +7203,7 @@ snapshots:
|
||||
package-manager-detector: 0.2.9
|
||||
picocolors: 1.1.1
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
spawndamnit: 3.0.1
|
||||
term-size: 2.2.1
|
||||
|
||||
@ -7226,7 +7226,7 @@ snapshots:
|
||||
'@changesets/types': 6.0.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
picocolors: 1.1.1
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
|
||||
'@changesets/get-release-plan@4.0.6':
|
||||
dependencies:
|
||||
@ -8423,10 +8423,10 @@ snapshots:
|
||||
dependencies:
|
||||
entities: 2.2.0
|
||||
|
||||
applesauce-accounts@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-accounts@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
applesauce-signers: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-signers: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
rxjs: 7.8.1
|
||||
@ -8434,13 +8434,13 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-content@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-content@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@cashu/cashu-ts': 2.0.0-rc1
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/unist': 3.0.3
|
||||
applesauce-core: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
remark: 15.0.1
|
||||
@ -8451,7 +8451,7 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-core@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-core@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@scure/base': 1.2.4
|
||||
debug: 4.4.0
|
||||
@ -8479,19 +8479,19 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-factory@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-factory@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
applesauce-content: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-content: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-loaders@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-loaders@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
applesauce-core: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
rx-nostr: 3.5.0
|
||||
@ -8510,12 +8510,12 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-react@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-react@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
applesauce-accounts: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-content: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-factory: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-accounts: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-content: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
applesauce-factory: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
react: 18.3.1
|
||||
rxjs: 7.8.1
|
||||
@ -8523,12 +8523,12 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-signers@0.0.0-next-20250203180810(typescript@5.7.3):
|
||||
applesauce-signers@0.0.0-next-20250203215539(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
'@noble/secp256k1': 1.7.1
|
||||
'@scure/base': 1.2.4
|
||||
applesauce-core: 0.0.0-next-20250203180810(typescript@5.7.3)
|
||||
applesauce-core: 0.0.0-next-20250203215539(typescript@5.7.3)
|
||||
debug: 4.4.0
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.3)
|
||||
@ -11073,7 +11073,7 @@ snapshots:
|
||||
|
||||
node-abi@3.74.0:
|
||||
dependencies:
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
|
||||
node-addon-api@6.1.0: {}
|
||||
|
||||
@ -11099,7 +11099,7 @@ snapshots:
|
||||
dependencies:
|
||||
hosted-git-info: 4.1.0
|
||||
is-core-module: 2.16.1
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
nostr-idb@2.2.0(typescript@5.7.3):
|
||||
@ -11990,7 +11990,7 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.0: {}
|
||||
semver@7.7.1: {}
|
||||
|
||||
send@0.19.0:
|
||||
dependencies:
|
||||
@ -12057,7 +12057,7 @@ snapshots:
|
||||
detect-libc: 2.0.3
|
||||
node-addon-api: 6.1.0
|
||||
prebuild-install: 7.1.3
|
||||
semver: 7.7.0
|
||||
semver: 7.7.1
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 3.0.8
|
||||
tunnel-agent: 0.6.0
|
||||
|
@ -8,9 +8,6 @@ import { createDefer, Deferred } from "applesauce-core/promise";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import PersistentSubscription from "./persistent-subscription";
|
||||
import Process from "./process";
|
||||
import processManager from "../services/process-manager";
|
||||
import Dataflow04 from "../components/icons/dataflow-04";
|
||||
import SuperMap from "./super-map";
|
||||
|
||||
/** Batches requests for events with #d tags from a single relay */
|
||||
@ -18,7 +15,6 @@ export default class BatchIdentifierLoader {
|
||||
store: EventStore;
|
||||
kinds: number[];
|
||||
relay: AbstractRelay;
|
||||
process: Process;
|
||||
|
||||
/** list of identifiers that have been loaded */
|
||||
requested = new Set<string>();
|
||||
@ -37,20 +33,17 @@ export default class BatchIdentifierLoader {
|
||||
|
||||
log: Debugger;
|
||||
|
||||
active = false;
|
||||
constructor(store: EventStore, relay: AbstractRelay, kinds: number[], log?: Debugger) {
|
||||
this.store = store;
|
||||
this.relay = relay;
|
||||
this.kinds = kinds;
|
||||
this.log = log || debug("BatchIdentifierLoader");
|
||||
this.process = new Process("BatchIdentifierLoader", this, [relay]);
|
||||
this.process.icon = Dataflow04;
|
||||
processManager.registerProcess(this.process);
|
||||
|
||||
this.subscription = new PersistentSubscription(this.relay, {
|
||||
onevent: (event) => this.handleEvent(event),
|
||||
oneose: () => this.handleEOSE(),
|
||||
});
|
||||
this.process.addChild(this.subscription.process);
|
||||
}
|
||||
|
||||
requestEvents(identifier: string): Promise<Map<string, NostrEvent>> {
|
||||
@ -74,9 +67,9 @@ export default class BatchIdentifierLoader {
|
||||
requestUpdate = _throttle(
|
||||
() => {
|
||||
// don't do anything if the subscription is already running
|
||||
if (this.process.active) return;
|
||||
if (this.active) return;
|
||||
|
||||
this.process.active = true;
|
||||
this.active = true;
|
||||
this.update();
|
||||
},
|
||||
500,
|
||||
@ -106,7 +99,7 @@ export default class BatchIdentifierLoader {
|
||||
|
||||
// reset
|
||||
this.pending.clear();
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
|
||||
for (const identifier of this.changedIdentifiers) {
|
||||
this.onIdentifierUpdate.next(identifier);
|
||||
@ -133,25 +126,23 @@ export default class BatchIdentifierLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
this.process.active = true;
|
||||
this.active = true;
|
||||
this.subscription.filters = [];
|
||||
if (dTags.length > 0) this.subscription.filters.push({ "#d": dTags, kinds: this.kinds });
|
||||
|
||||
await this.subscription.update();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) this.log(`Failed to update subscription`, error.message);
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
}
|
||||
} else {
|
||||
this.log("Closing");
|
||||
this.subscription.close();
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.subscription.destroy();
|
||||
this.process.remove();
|
||||
processManager.unregisterProcess(this.process);
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,6 @@ import { createDefer, Deferred } from "applesauce-core/promise";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import PersistentSubscription from "./persistent-subscription";
|
||||
import Process from "./process";
|
||||
import processManager from "../services/process-manager";
|
||||
import Dataflow04 from "../components/icons/dataflow-04";
|
||||
import SuperMap from "./super-map";
|
||||
import { eventStore } from "../services/event-store";
|
||||
|
||||
@ -16,7 +13,6 @@ import { eventStore } from "../services/event-store";
|
||||
export default class BatchRelationLoader {
|
||||
kinds: number[];
|
||||
relay: AbstractRelay;
|
||||
process: Process;
|
||||
|
||||
requested = new Set<string>();
|
||||
/** event id / coordinate -> event id -> event */
|
||||
@ -34,19 +30,16 @@ export default class BatchRelationLoader {
|
||||
|
||||
log: Debugger;
|
||||
|
||||
active = false;
|
||||
constructor(relay: AbstractRelay, kinds: number[], log?: Debugger) {
|
||||
this.relay = relay;
|
||||
this.kinds = kinds;
|
||||
this.log = log || debug("BatchRelationLoader");
|
||||
this.process = new Process("BatchRelationLoader", this, [relay]);
|
||||
this.process.icon = Dataflow04;
|
||||
processManager.registerProcess(this.process);
|
||||
|
||||
this.subscription = new PersistentSubscription(this.relay, {
|
||||
onevent: (event) => this.handleEvent(event),
|
||||
oneose: () => this.handleEOSE(),
|
||||
});
|
||||
this.process.addChild(this.subscription.process);
|
||||
}
|
||||
|
||||
requestEvents(uid: string): Promise<Map<string, NostrEvent>> {
|
||||
@ -70,9 +63,9 @@ export default class BatchRelationLoader {
|
||||
requestUpdate = _throttle(
|
||||
() => {
|
||||
// don't do anything if the subscription is already running
|
||||
if (this.process.active) return;
|
||||
if (this.active) return;
|
||||
|
||||
this.process.active = true;
|
||||
this.active = true;
|
||||
this.update();
|
||||
},
|
||||
500,
|
||||
@ -106,7 +99,7 @@ export default class BatchRelationLoader {
|
||||
|
||||
// reset
|
||||
this.pending.clear();
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
|
||||
// do next request or close the subscription
|
||||
if (this.next.size > 0) this.requestUpdate();
|
||||
@ -132,7 +125,7 @@ export default class BatchRelationLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
this.process.active = true;
|
||||
this.active = true;
|
||||
this.subscription.filters = [];
|
||||
if (ids.length > 0) this.subscription.filters.push({ "#e": ids, kinds: this.kinds });
|
||||
if (cords.length > 0) this.subscription.filters.push({ "#a": cords, kinds: this.kinds });
|
||||
@ -140,18 +133,16 @@ export default class BatchRelationLoader {
|
||||
await this.subscription.update();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) this.log(`Failed to update subscription`, error.message);
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
}
|
||||
} else {
|
||||
this.log("Closing");
|
||||
this.subscription.close();
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.subscription.destroy();
|
||||
this.process.remove();
|
||||
processManager.unregisterProcess(this.process);
|
||||
}
|
||||
}
|
||||
|
@ -1,167 +0,0 @@
|
||||
import { NostrEvent, kinds, nip18, nip25 } from "nostr-tools";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { map, throttleTime } from "rxjs/operators";
|
||||
import { getZapPayment } from "applesauce-core/helpers";
|
||||
|
||||
import { getContentPointers, getThreadReferences, isReply, isRepost } from "../helpers/nostr/event";
|
||||
import singleEventLoader from "../services/single-event-loader";
|
||||
import { getPubkeysMentionedInContent } from "../helpers/nostr/post";
|
||||
import { TORRENT_COMMENT_KIND } from "../helpers/nostr/torrents";
|
||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||
import { eventStore, queryStore } from "../services/event-store";
|
||||
import localSettings from "../services/local-settings";
|
||||
|
||||
export const NotificationTypeSymbol = Symbol("notificationType");
|
||||
|
||||
export enum NotificationType {
|
||||
Reply = "reply",
|
||||
Repost = "repost",
|
||||
Zap = "zap",
|
||||
Reaction = "reaction",
|
||||
Mention = "mention",
|
||||
Message = "message",
|
||||
Quote = "quote",
|
||||
}
|
||||
export type CategorizedEvent = NostrEvent & { [NotificationTypeSymbol]?: NotificationType };
|
||||
|
||||
export default class AccountNotifications {
|
||||
pubkey: string;
|
||||
|
||||
timeline = new BehaviorSubject<CategorizedEvent[]>([]);
|
||||
|
||||
constructor(pubkey: string) {
|
||||
this.pubkey = pubkey;
|
||||
|
||||
// subscribe to query store
|
||||
queryStore
|
||||
.timeline([
|
||||
{
|
||||
"#p": [pubkey],
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Repost,
|
||||
kinds.GenericRepost,
|
||||
kinds.Reaction,
|
||||
kinds.Zap,
|
||||
TORRENT_COMMENT_KIND,
|
||||
kinds.LongFormArticle,
|
||||
kinds.EncryptedDirectMessage,
|
||||
1111, //NIP-22
|
||||
],
|
||||
},
|
||||
])
|
||||
.pipe(
|
||||
throttleTime(100),
|
||||
map((events) => events.map(this.handleEvent.bind(this)).filter(this.filterEvent.bind(this))),
|
||||
)
|
||||
.subscribe((events) => this.timeline.next(events));
|
||||
}
|
||||
|
||||
private categorizeEvent(event: NostrEvent): CategorizedEvent {
|
||||
const e = event as CategorizedEvent;
|
||||
|
||||
if (e[NotificationTypeSymbol]) return e;
|
||||
|
||||
if (event.kind === kinds.Zap) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Zap;
|
||||
} else if (event.kind === kinds.Reaction) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Reaction;
|
||||
} else if (isRepost(event)) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Repost;
|
||||
} else if (event.kind === kinds.EncryptedDirectMessage) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Message;
|
||||
} else if (
|
||||
event.kind === kinds.ShortTextNote ||
|
||||
event.kind === TORRENT_COMMENT_KIND ||
|
||||
event.kind === kinds.LiveChatMessage ||
|
||||
event.kind === kinds.LongFormArticle
|
||||
) {
|
||||
// is the pubkey mentioned in any way in the content
|
||||
const isMentioned = getPubkeysMentionedInContent(event.content, true).includes(this.pubkey);
|
||||
const isQuote =
|
||||
event.tags.some((t) => t[0] === "q" && (t[1] === event.id || t[3] === this.pubkey)) ||
|
||||
getContentPointers(event.content).some(
|
||||
(p) => (p.type === "nevent" && p.data.id === event.id) || (p.type === "note" && p.data === event.id),
|
||||
);
|
||||
|
||||
if (isMentioned) e[NotificationTypeSymbol] = NotificationType.Mention;
|
||||
else if (isQuote) e[NotificationTypeSymbol] = NotificationType.Quote;
|
||||
else if (isReply(event)) e[NotificationTypeSymbol] = NotificationType.Reply;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
handleEvent(event: NostrEvent) {
|
||||
const e = this.categorizeEvent(event);
|
||||
|
||||
const loadEvent = (eventId: string, relays?: string[]) => {
|
||||
singleEventLoader.next({ id: eventId, relays: [...localSettings.readRelays.value, ...(relays ?? [])] });
|
||||
};
|
||||
|
||||
// load event quotes
|
||||
const quotes = event.tags.filter((t) => t[0] === "q" && t[1]);
|
||||
for (const tag of quotes) {
|
||||
loadEvent(tag[1], tag[2] ? [tag[2]] : undefined);
|
||||
}
|
||||
|
||||
// load reactions and replies
|
||||
switch (e[NotificationTypeSymbol]) {
|
||||
case NotificationType.Reply:
|
||||
const refs = getThreadReferences(e);
|
||||
if (refs.reply?.e?.id) loadEvent(refs.reply.e.id, refs.reply.e.relays);
|
||||
break;
|
||||
case NotificationType.Reaction: {
|
||||
const pointer = nip25.getReactedEventPointer(e);
|
||||
if (pointer?.id) loadEvent(pointer.id, pointer.relays);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
private filterEvent(event: CategorizedEvent) {
|
||||
// ignore if muted
|
||||
// TODO: this should be moved somewhere more performant
|
||||
const muteList = eventStore.getReplaceable(kinds.Mutelist, this.pubkey);
|
||||
const mutedPubkeys = muteList ? getPubkeysFromList(muteList).map((p) => p.pubkey) : [];
|
||||
if (mutedPubkeys.includes(event.pubkey)) return false;
|
||||
|
||||
// ignore if own
|
||||
if (event.pubkey === this.pubkey) return false;
|
||||
|
||||
const e = event as CategorizedEvent;
|
||||
|
||||
switch (e[NotificationTypeSymbol]) {
|
||||
case NotificationType.Reply:
|
||||
const refs = getThreadReferences(e);
|
||||
if (!refs.reply?.e?.id) return false;
|
||||
if (refs.reply?.e?.author && refs.reply?.e?.author !== this.pubkey) return false;
|
||||
const parent = eventStore.getEvent(refs.reply.e.id);
|
||||
if (!parent || parent.pubkey !== this.pubkey) return false;
|
||||
break;
|
||||
case NotificationType.Mention:
|
||||
break;
|
||||
case NotificationType.Repost: {
|
||||
const pointer = nip18.getRepostedEventPointer(e);
|
||||
if (pointer?.author !== this.pubkey) return false;
|
||||
break;
|
||||
}
|
||||
case NotificationType.Reaction: {
|
||||
const pointer = nip25.getReactedEventPointer(e);
|
||||
if (!pointer) return false;
|
||||
if (pointer.author !== this.pubkey) return false;
|
||||
if (pointer.kind === kinds.EncryptedDirectMessage) return false;
|
||||
const parent = eventStore.getEvent(pointer.id);
|
||||
if (parent && parent.kind === kinds.EncryptedDirectMessage) return false;
|
||||
break;
|
||||
}
|
||||
case NotificationType.Zap:
|
||||
const p = getZapPayment(e);
|
||||
if (!p || p.amount === 0) return false;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -4,13 +4,9 @@ import { AbstractRelay, Subscription, SubscriptionParams } from "nostr-tools/abs
|
||||
import { isFilterEqual } from "applesauce-core/helpers";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import FilterFunnel01 from "../components/icons/filter-funnel-01";
|
||||
import processManager from "../services/process-manager";
|
||||
import Process from "./process";
|
||||
|
||||
export default class PersistentSubscription {
|
||||
id: string;
|
||||
process: Process;
|
||||
relay: Relay;
|
||||
filters: Filter[];
|
||||
connecting = false;
|
||||
@ -24,10 +20,9 @@ export default class PersistentSubscription {
|
||||
return !this.subscription || this.subscription.closed;
|
||||
}
|
||||
|
||||
active = false;
|
||||
constructor(relay: AbstractRelay, params?: Partial<SubscriptionParams>) {
|
||||
this.id = nanoid(8);
|
||||
this.process = new Process("PersistentSubscription", this, [relay]);
|
||||
this.process.icon = FilterFunnel01;
|
||||
this.filters = [];
|
||||
this.params = {
|
||||
//@ts-expect-error
|
||||
@ -36,8 +31,6 @@ export default class PersistentSubscription {
|
||||
};
|
||||
|
||||
this.relay = relay;
|
||||
|
||||
processManager.registerProcess(this.process);
|
||||
}
|
||||
|
||||
/** attempts to update the subscription */
|
||||
@ -45,12 +38,12 @@ export default class PersistentSubscription {
|
||||
if (!this.filters || this.filters.length === 0) throw new Error("Missing filters");
|
||||
if (this.connecting) throw new Error("Cant update while connecting");
|
||||
|
||||
this.process.active = true;
|
||||
this.active = true;
|
||||
|
||||
this.connecting = true;
|
||||
if ((await relayPoolService.waitForOpen(this.relay)) === false) {
|
||||
this.connecting = false;
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
throw new Error("Failed to connect to relay");
|
||||
}
|
||||
this.connecting = false;
|
||||
@ -66,7 +59,7 @@ export default class PersistentSubscription {
|
||||
if (!this.closed) {
|
||||
relayPoolService.handleRelayNotice(this.relay, reason);
|
||||
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
}
|
||||
this.params.onclose?.(reason);
|
||||
},
|
||||
@ -80,14 +73,12 @@ export default class PersistentSubscription {
|
||||
}
|
||||
close() {
|
||||
if (this.subscription?.closed === false) this.subscription.close();
|
||||
this.process.active = false;
|
||||
this.active = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.close();
|
||||
this.process.remove();
|
||||
processManager.unregisterProcess(this.process);
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
import { ComponentWithAs, IconProps } from "@chakra-ui/react";
|
||||
import { SimpleRelay } from "nostr-idb";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
let lastId = 0;
|
||||
export default class Process {
|
||||
id = ++lastId;
|
||||
type: string;
|
||||
name?: string;
|
||||
icon?: ComponentWithAs<"svg", IconProps>;
|
||||
source: any;
|
||||
|
||||
// if this process is running
|
||||
active: boolean = false;
|
||||
|
||||
// the relays this process is claiming
|
||||
relays = new Set<AbstractRelay | SimpleRelay>();
|
||||
|
||||
// the parent process
|
||||
parent?: Process;
|
||||
// any children this process has created
|
||||
children = new Set<Process>();
|
||||
|
||||
constructor(type: string, source: any, relays?: Iterable<AbstractRelay | SimpleRelay>) {
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
|
||||
this.relays = new Set(relays);
|
||||
}
|
||||
|
||||
static forkOrCreate(name: string, source: any, relays: Iterable<AbstractRelay | SimpleRelay>, parent?: Process) {
|
||||
return parent?.fork(name, source, relays) || new Process("BatchKindLoader", this, relays);
|
||||
}
|
||||
|
||||
addChild(child: Process) {
|
||||
if (child === this) throw new Error("Process cant be a child of itself");
|
||||
this.children.add(child);
|
||||
child.parent = this;
|
||||
}
|
||||
|
||||
fork(name: string, source: any, relays?: Iterable<AbstractRelay | SimpleRelay>) {
|
||||
const child = new Process(name, source, relays);
|
||||
this.addChild(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (!this.parent) return;
|
||||
|
||||
this.parent.children.delete(this);
|
||||
this.parent = undefined;
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import { logger } from "../helpers/debug";
|
||||
import { safeRelayUrl, validateRelayURL } from "../helpers/relay";
|
||||
import SuperMap from "./super-map";
|
||||
import verifyEventMethod from "../services/verify-event";
|
||||
import processManager from "../services/process-manager";
|
||||
import localSettings from "../services/local-settings";
|
||||
|
||||
export type Notice = {
|
||||
@ -229,13 +228,7 @@ export default class RelayPool implements IConnectionPool {
|
||||
// don't disconnect from authenticated relays
|
||||
if (this.authenticated.get(relay).value) continue;
|
||||
|
||||
let disconnect = true;
|
||||
for (const process of processManager.processes) {
|
||||
if (process.active && process.relays.has(relay)) {
|
||||
disconnect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let disconnect = false;
|
||||
|
||||
if (disconnect) {
|
||||
this.log(`No active processes using ${relay.url}, disconnecting`);
|
||||
|
@ -95,21 +95,17 @@ export function EmbeddedImage({ src, event, imageProps, ...props }: EmbeddedImag
|
||||
if (ref.current) handleImageFallbacks(ref.current, getPubkeyMediaServers);
|
||||
}, []);
|
||||
|
||||
// NOTE: the parent <div> has display=block and and <a> has inline-block
|
||||
// this is so that the <a> element can act like a block without being full width
|
||||
return (
|
||||
<div>
|
||||
<Link href={src} isExternal onClick={handleClick} display="inline-block" {...props}>
|
||||
<TrustImage
|
||||
{...imageProps}
|
||||
src={thumbnail}
|
||||
cursor="pointer"
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
data-pubkey={owner}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href={src} isExternal onClick={handleClick} display="inline-block" {...props}>
|
||||
<TrustImage
|
||||
{...imageProps}
|
||||
src={thumbnail}
|
||||
cursor="pointer"
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
data-pubkey={owner}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@ -184,7 +180,6 @@ export function renderImageUrl(match: URL) {
|
||||
label="Image"
|
||||
url={match}
|
||||
actions={hash ? <VerifyImageButton src={match} original={hash} zIndex={1} /> : undefined}
|
||||
hideOnDefaultOpen={!hash}
|
||||
>
|
||||
<EmbeddedImage src={match.toString()} imageProps={{ maxH: ["initial", "35vh"] }} />
|
||||
</ExpandableEmbed>
|
||||
|
@ -38,7 +38,7 @@ export function renderVideoUrl(match: URL) {
|
||||
if (!isVideoURL(match)) return null;
|
||||
|
||||
return (
|
||||
<ExpandableEmbed label="Video" url={match} hideOnDefaultOpen>
|
||||
<ExpandableEmbed label="Video" url={match}>
|
||||
<TrustVideo src={match.toString()} maxH="lg" w="auto" />
|
||||
</ExpandableEmbed>
|
||||
);
|
||||
@ -48,7 +48,7 @@ export function renderStreamUrl(match: URL) {
|
||||
if (!isStreamURL(match)) return null;
|
||||
|
||||
return (
|
||||
<ExpandableEmbed label="Video" url={match} hideOnDefaultOpen>
|
||||
<ExpandableEmbed label="Video" url={match}>
|
||||
<LiveVideoPlayer stream={match.toString()} maxH="lg" w="auto" />
|
||||
</ExpandableEmbed>
|
||||
);
|
||||
|
@ -7,7 +7,7 @@ import { QuestionIcon } from "@chakra-ui/icons";
|
||||
import { LightningIcon, SettingsIcon } from "../../icons";
|
||||
import Package from "../../icons/package";
|
||||
import useRecentIds from "../../../hooks/use-recent-ids";
|
||||
import { defaultFavoriteApps, internalApps, internalTools } from "../../navigation/apps";
|
||||
import { defaultAnonFavoriteApps, defaultUserFavoriteApps, internalApps, internalTools } from "../../navigation/apps";
|
||||
import NavItem from "./nav-item";
|
||||
import Plus from "../../icons/plus";
|
||||
import useFavoriteInternalIds from "../../../hooks/use-favorite-internal-ids";
|
||||
@ -15,7 +15,8 @@ import useFavoriteInternalIds from "../../../hooks/use-favorite-internal-ids";
|
||||
export default function NavItems() {
|
||||
const account = useActiveAccount();
|
||||
|
||||
const { ids: favorites = defaultFavoriteApps } = useFavoriteInternalIds("apps", "app");
|
||||
const defaultApps = account ? defaultUserFavoriteApps : defaultAnonFavoriteApps;
|
||||
const { ids: favorites = defaultApps } = useFavoriteInternalIds("apps", "app");
|
||||
const { recent } = useRecentIds("apps", 3);
|
||||
|
||||
const favoriteApps = useMemo(() => {
|
||||
|
@ -4,7 +4,7 @@ import { kinds } from "nostr-tools";
|
||||
import { useEventFactory } from "applesauce-react/hooks";
|
||||
import { removeNameValueTag, addNameValueTag } from "applesauce-factory/operations";
|
||||
|
||||
import { App, defaultFavoriteApps } from "./apps";
|
||||
import { App, defaultUserFavoriteApps } from "./apps";
|
||||
import useFavoriteInternalIds from "../../hooks/use-favorite-internal-ids";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
import { StarEmptyIcon, StarFullIcon } from "../icons";
|
||||
@ -22,7 +22,7 @@ export default function AppFavoriteButton({
|
||||
const handleClick = async () => {
|
||||
const prev = favorites || {
|
||||
kind: kinds.Application,
|
||||
tags: [["d", "nostrudel-favorite-apps"], ...defaultFavoriteApps.map((id) => ["app", id])],
|
||||
tags: [["d", "nostrudel-favorite-apps"], ...defaultUserFavoriteApps.map((id) => ["app", id])],
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
|
@ -260,6 +260,7 @@ export const externalTools: App[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultFavoriteApps = ["launchpad", "notes", "discover", "notifications", "messages", "search"];
|
||||
export const defaultAnonFavoriteApps = ["notes", "discover", "search", "articles", "streams"];
|
||||
export const defaultUserFavoriteApps = ["launchpad", "notes", "discover", "notifications", "messages", "search"];
|
||||
|
||||
export const allApps = [...internalApps, ...internalTools, ...externalTools];
|
||||
|
@ -59,7 +59,7 @@ export default function PeopleListSelection({
|
||||
const modal = useDisclosure();
|
||||
const account = useActiveAccount();
|
||||
const lists = useUserSets(account?.pubkey).filter((list) => list.kind === kinds.Followsets);
|
||||
const { lists: favoriteLists } = useFavoriteLists();
|
||||
const { lists: favoriteLists } = useFavoriteLists(account?.pubkey);
|
||||
const { selected, setSelected, listEvent } = usePeopleListContext();
|
||||
|
||||
const searchDirectory = useObservable(userSearchDirectory);
|
||||
|
@ -12,14 +12,6 @@ export default function useForwardSubscription(relays: string[], filters?: Filte
|
||||
const id = useMemo(() => nanoid(10), []);
|
||||
const rxReq = useMemo(() => createRxForwardReq(id), [id]);
|
||||
|
||||
// load from cache
|
||||
// useEffect(() => {
|
||||
// const sub = cacheRequest(Array.isArray(filters) ? filters : [filters]).subscribe({
|
||||
// next: (e) => eventStore.add(e),
|
||||
// complete: () => sub.unsubscribe(),
|
||||
// });
|
||||
// }, [hash(filters)]);
|
||||
|
||||
// attach to rxNostr
|
||||
const observable = useMemo(() => rxNostr.use(rxReq, { on: { relays } }), [rxReq, relays.join(",")]);
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { useStoreQuery } from "applesauce-react/hooks";
|
||||
import { useEventStore } from "applesauce-react/hooks/use-event-store";
|
||||
import { Queries } from "applesauce-core";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { useThrottle } from "react-use";
|
||||
import sum from "hash-sum";
|
||||
|
||||
import timelineCacheService from "../services/timeline-cache";
|
||||
@ -35,16 +36,17 @@ export default function useTimelineLoader(
|
||||
return () => sub?.unsubscribe();
|
||||
}, [eventStore, loader]);
|
||||
|
||||
let timeline = useStoreQuery(Queries.TimelineQuery, filters && [filters]) ?? [];
|
||||
const timeline = useStoreQuery(Queries.TimelineQuery, filters && [filters]) ?? [];
|
||||
let throttled = useThrottle(timeline, 50);
|
||||
|
||||
// set event filter
|
||||
if (opts?.eventFilter)
|
||||
timeline = timeline.filter((e) => {
|
||||
throttled = throttled.filter((e) => {
|
||||
try {
|
||||
return opts.eventFilter && opts.eventFilter(e);
|
||||
} catch (error) {}
|
||||
return false;
|
||||
});
|
||||
|
||||
return { loader, timeline };
|
||||
return { loader, timeline: throttled };
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { AccountsProvider, QueryStoreProvider } from "applesauce-react/providers
|
||||
import { SigningProvider } from "./signing-provider";
|
||||
import buildTheme from "../../theme";
|
||||
import useAppSettings from "../../hooks/use-user-app-settings";
|
||||
import NotificationsProvider from "./notifications-provider";
|
||||
import { UserEmojiProvider } from "./emoji-provider";
|
||||
import BreakpointProvider from "./breakpoint-provider";
|
||||
import PublishProvider from "./publish-provider";
|
||||
@ -33,13 +32,11 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
|
||||
<ThemeProviders>
|
||||
<SigningProvider>
|
||||
<PublishProvider>
|
||||
<NotificationsProvider>
|
||||
<UserEmojiProvider>
|
||||
<EventFactoryProvider>
|
||||
<WebOfTrustProvider>{children}</WebOfTrustProvider>
|
||||
</EventFactoryProvider>
|
||||
</UserEmojiProvider>
|
||||
</NotificationsProvider>
|
||||
<UserEmojiProvider>
|
||||
<EventFactoryProvider>
|
||||
<WebOfTrustProvider>{children}</WebOfTrustProvider>
|
||||
</EventFactoryProvider>
|
||||
</UserEmojiProvider>
|
||||
</PublishProvider>
|
||||
</SigningProvider>
|
||||
</ThemeProviders>
|
||||
|
@ -1,80 +0,0 @@
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { useActiveAccount } from "applesauce-react/hooks";
|
||||
import { TimelineLoader } from "applesauce-loaders";
|
||||
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
||||
import { useUserInbox } from "../../hooks/use-user-mailboxes";
|
||||
import AccountNotifications from "../../classes/notifications";
|
||||
import { truncateId } from "../../helpers/string";
|
||||
|
||||
type NotificationTimelineContextType = {
|
||||
timeline?: TimelineLoader;
|
||||
notifications?: AccountNotifications;
|
||||
};
|
||||
const NotificationTimelineContext = createContext<NotificationTimelineContextType | null>(null);
|
||||
|
||||
export function useNotifications() {
|
||||
const ctx = useContext(NotificationTimelineContext);
|
||||
if (!ctx) throw new Error("Missing notifications provider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export default function NotificationsProvider({ children }: PropsWithChildren) {
|
||||
const account = useActiveAccount();
|
||||
const inbox = useUserInbox(account?.pubkey);
|
||||
const readRelays = useReadRelays(inbox);
|
||||
|
||||
const [notifications, setNotifications] = useState<AccountNotifications>();
|
||||
|
||||
const userMuteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (userMuteFilter(event)) return false;
|
||||
return true;
|
||||
},
|
||||
[userMuteFilter],
|
||||
);
|
||||
|
||||
const { loader } = useTimelineLoader(
|
||||
`${truncateId(account?.pubkey ?? "anon")}-notification`,
|
||||
readRelays,
|
||||
account?.pubkey
|
||||
? {
|
||||
"#p": [account.pubkey],
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Repost,
|
||||
kinds.GenericRepost,
|
||||
kinds.Reaction,
|
||||
kinds.Zap,
|
||||
TORRENT_COMMENT_KIND,
|
||||
kinds.LongFormArticle,
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
{ eventFilter },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!account?.pubkey) return;
|
||||
const n = new AccountNotifications(account.pubkey);
|
||||
setNotifications(n);
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-expect-error debug
|
||||
window.accountNotifications = n;
|
||||
}
|
||||
|
||||
return () => {
|
||||
setNotifications(undefined);
|
||||
};
|
||||
}, [account?.pubkey]);
|
||||
|
||||
const context = useMemo(() => ({ timeline: loader, notifications }), [loader, notifications]);
|
||||
|
||||
return <NotificationTimelineContext.Provider value={context}>{children}</NotificationTimelineContext.Provider>;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
|
||||
import { Filter, kinds } from "nostr-tools";
|
||||
import { getProfilePointersFromList } from "applesauce-core/helpers";
|
||||
import { useActiveAccount } from "applesauce-react/hooks";
|
||||
import { Filter, kinds } from "nostr-tools";
|
||||
|
||||
import { getPubkeysFromList } from "../../helpers/nostr/lists";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
||||
@ -46,15 +46,16 @@ export function usePeopleListSelect(selected: ListId, onChange: (list: ListId) =
|
||||
const listId = useListCoordinate(selected);
|
||||
const listEvent = useReplaceableEvent(listId, [], true);
|
||||
|
||||
const people = listEvent && getPubkeysFromList(listEvent);
|
||||
const people = useMemo(() => listEvent && getProfilePointersFromList(listEvent), [listEvent]);
|
||||
|
||||
const filter = useMemo<Filter | undefined>(() => {
|
||||
if (selected === "global") return {};
|
||||
if (selected === "self") {
|
||||
if (account) return { authors: [account.pubkey] };
|
||||
else return {};
|
||||
else return undefined;
|
||||
}
|
||||
if (!people) return undefined;
|
||||
if (!people || people.length === 0) return undefined;
|
||||
|
||||
return { authors: people.map((p) => p.pubkey) };
|
||||
}, [people, selected, account]);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BehaviorSubject, distinctUntilChanged, pairwise } from "rxjs";
|
||||
import { BehaviorSubject, distinctUntilChanged, Observable, pairwise } from "rxjs";
|
||||
import { CacheRelay, openDB } from "nostr-idb";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import { fakeVerifyEvent, isFromCache } from "applesauce-core/helpers";
|
||||
@ -10,6 +10,7 @@ import WasmRelay from "./wasm-relay";
|
||||
import MemoryRelay from "../classes/memory-relay";
|
||||
import localSettings from "./local-settings";
|
||||
import { eventStore } from "./event-store";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
|
||||
export const NOSTR_RELAY_TRAY_URL = "ws://localhost:4869/";
|
||||
export async function checkNostrRelayTray() {
|
||||
@ -114,6 +115,23 @@ cacheRelay$.pipe(pairwise()).subscribe(([prev, current]) => {
|
||||
}
|
||||
});
|
||||
|
||||
// load events from cache relay
|
||||
export function cacheRequest(filters: Filter[]) {
|
||||
return new Observable<NostrEvent>((observer) => {
|
||||
const relay = getCacheRelay();
|
||||
if (!relay) return observer.complete();
|
||||
|
||||
const sub = relay.subscribe(filters, {
|
||||
onevent: (event) => observer.next(event),
|
||||
oneose: () => {
|
||||
sub.close();
|
||||
observer.complete();
|
||||
},
|
||||
onclose: () => observer.complete(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** set the cache relay URL and waits for it to connect */
|
||||
export async function setCacheRelayURL(url: string) {
|
||||
return new Promise<void>((res) => {
|
||||
|
@ -5,18 +5,14 @@ import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { WIKI_PAGE_KIND } from "../helpers/nostr/wiki";
|
||||
import { logger } from "../helpers/debug";
|
||||
import Process from "../classes/process";
|
||||
import SuperMap from "../classes/super-map";
|
||||
import BatchIdentifierLoader from "../classes/batch-identifier-loader";
|
||||
import BookOpen01 from "../components/icons/book-open-01";
|
||||
import processManager from "./process-manager";
|
||||
import relayPoolService from "./relay-pool";
|
||||
import { eventStore } from "./event-store";
|
||||
import { getCacheRelay } from "./cache-relay";
|
||||
|
||||
class DictionaryService {
|
||||
log = logger.extend("DictionaryService");
|
||||
process: Process;
|
||||
store: EventStore;
|
||||
|
||||
topics = new SuperMap<string, BehaviorSubject<Map<string, NostrEvent>>>(
|
||||
@ -25,7 +21,6 @@ class DictionaryService {
|
||||
|
||||
loaders = new SuperMap<AbstractRelay, BatchIdentifierLoader>((relay) => {
|
||||
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);
|
||||
});
|
||||
@ -34,11 +29,6 @@ class DictionaryService {
|
||||
|
||||
constructor(store: EventStore) {
|
||||
this.store = store;
|
||||
this.process = new Process("DictionaryService", this);
|
||||
this.process.icon = BookOpen01;
|
||||
this.process.active = true;
|
||||
|
||||
processManager.registerProcess(this.process);
|
||||
}
|
||||
|
||||
// merged results from all loaders for a single event
|
||||
|
@ -4,32 +4,18 @@ import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import SuperMap from "../classes/super-map";
|
||||
import relayPoolService from "./relay-pool";
|
||||
import Process from "../classes/process";
|
||||
import { LightningIcon } from "../components/icons";
|
||||
import processManager from "./process-manager";
|
||||
import BatchRelationLoader from "../classes/batch-relation-loader";
|
||||
import { logger } from "../helpers/debug";
|
||||
import { getCacheRelay } from "./cache-relay";
|
||||
|
||||
class EventReactionsService {
|
||||
log = logger.extend("EventReactionsService");
|
||||
process: Process;
|
||||
|
||||
private loaded = new Map<string, boolean>();
|
||||
loaders = new SuperMap<AbstractRelay, BatchRelationLoader>((relay) => {
|
||||
const loader = new BatchRelationLoader(relay, [kinds.Reaction], this.log.extend(relay.url));
|
||||
this.process.addChild(loader.process);
|
||||
return loader;
|
||||
return new BatchRelationLoader(relay, [kinds.Reaction], this.log.extend(relay.url));
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.process = new Process("EventReactionsService", this);
|
||||
this.process.icon = LightningIcon;
|
||||
this.process.active = true;
|
||||
|
||||
processManager.registerProcess(this.process);
|
||||
}
|
||||
|
||||
requestReactions(uid: string, urls: Iterable<string | URL | AbstractRelay>, alwaysRequest = false) {
|
||||
if (this.loaded.get(uid) && !alwaysRequest) return;
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { EventStore, QueryStore } from "applesauce-core";
|
||||
import { isFromCache } from "applesauce-core/helpers";
|
||||
|
||||
import { cacheRelay$ } from "./cache-relay";
|
||||
|
||||
export const eventStore = new EventStore();
|
||||
export const queryStore = new QueryStore(eventStore);
|
||||
@ -12,10 +9,3 @@ if (import.meta.env.DEV) {
|
||||
// @ts-expect-error debug
|
||||
window.queryStore = queryStore;
|
||||
}
|
||||
|
||||
// save all events to cache relay
|
||||
eventStore.database.inserted.subscribe((event) => {
|
||||
if (!isFromCache(event) && cacheRelay$.value) {
|
||||
cacheRelay$.value.publish(event);
|
||||
}
|
||||
});
|
||||
|
@ -4,32 +4,19 @@ import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import SuperMap from "../classes/super-map";
|
||||
import relayPoolService from "./relay-pool";
|
||||
import Process from "../classes/process";
|
||||
import { LightningIcon } from "../components/icons";
|
||||
import processManager from "./process-manager";
|
||||
import BatchRelationLoader from "../classes/batch-relation-loader";
|
||||
import { logger } from "../helpers/debug";
|
||||
import { getCacheRelay } from "./cache-relay";
|
||||
|
||||
class EventZapsService {
|
||||
log = logger.extend("EventZapsService");
|
||||
process: Process;
|
||||
|
||||
private loaded = new Map<string, boolean>();
|
||||
loaders = new SuperMap<AbstractRelay, BatchRelationLoader>((relay) => {
|
||||
const loader = new BatchRelationLoader(relay, [kinds.Zap], this.log.extend(relay.url));
|
||||
this.process.addChild(loader.process);
|
||||
return loader;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.process = new Process("EventZapsService", this);
|
||||
this.process.icon = LightningIcon;
|
||||
this.process.active = true;
|
||||
|
||||
processManager.registerProcess(this.process);
|
||||
}
|
||||
|
||||
requestZaps(uid: string, urls: Iterable<string | URL | AbstractRelay>, alwaysRequest = true) {
|
||||
if (this.loaded.get(uid) && !alwaysRequest) return;
|
||||
|
||||
|
@ -1,13 +1,114 @@
|
||||
import { getEventPointerFromETag, getEventPointerFromQTag, processTags } from "applesauce-core/helpers";
|
||||
import { combineLatest, mergeMap, tap } from "rxjs";
|
||||
import { TimelineQuery } from "applesauce-core/queries";
|
||||
import { kinds, NostrEvent } from "nostr-tools";
|
||||
import {
|
||||
COMMENT_KIND,
|
||||
getEventPointerFromETag,
|
||||
getEventPointerFromQTag,
|
||||
getZapPayment,
|
||||
Mutes,
|
||||
processTags,
|
||||
} from "applesauce-core/helpers";
|
||||
import { combineLatest, map, mergeMap, Observable, share, tap } from "rxjs";
|
||||
import { TimelineQuery, UserMuteQuery } from "applesauce-core/queries";
|
||||
import { kinds, nip18, nip25, NostrEvent } from "nostr-tools";
|
||||
|
||||
import localSettings from "./local-settings";
|
||||
import singleEventLoader from "./single-event-loader";
|
||||
import { queryStore } from "./event-store";
|
||||
import { eventStore, queryStore } from "./event-store";
|
||||
import { TORRENT_COMMENT_KIND } from "../helpers/nostr/torrents";
|
||||
import accounts from "./accounts";
|
||||
import { getThreadReferences, isReply, isRepost } from "../helpers/nostr/event";
|
||||
import { getPubkeysMentionedInContent } from "../helpers/nostr/post";
|
||||
import { getContentPointers } from "applesauce-factory/helpers";
|
||||
|
||||
export const NotificationTypeSymbol = Symbol("notificationType");
|
||||
|
||||
export enum NotificationType {
|
||||
Reply = "reply",
|
||||
Repost = "repost",
|
||||
Zap = "zap",
|
||||
Reaction = "reaction",
|
||||
Mention = "mention",
|
||||
Message = "message",
|
||||
Quote = "quote",
|
||||
}
|
||||
export type CategorizedEvent = NostrEvent & { [NotificationTypeSymbol]?: NotificationType };
|
||||
|
||||
function categorizeEvent(event: NostrEvent, pubkey?: string): CategorizedEvent {
|
||||
const e = event as CategorizedEvent;
|
||||
|
||||
if (e[NotificationTypeSymbol]) return e;
|
||||
|
||||
if (event.kind === kinds.Zap) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Zap;
|
||||
} else if (event.kind === kinds.Reaction) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Reaction;
|
||||
} else if (isRepost(event)) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Repost;
|
||||
} else if (event.kind === kinds.EncryptedDirectMessage) {
|
||||
e[NotificationTypeSymbol] = NotificationType.Message;
|
||||
} else if (
|
||||
event.kind === kinds.ShortTextNote ||
|
||||
event.kind === TORRENT_COMMENT_KIND ||
|
||||
event.kind === kinds.LiveChatMessage ||
|
||||
event.kind === kinds.LongFormArticle
|
||||
) {
|
||||
// is the pubkey mentioned in any way in the content
|
||||
const isMentioned = pubkey ? getPubkeysMentionedInContent(event.content, true).includes(pubkey) : false;
|
||||
const isQuote =
|
||||
event.tags.some((t) => t[0] === "q" && (t[1] === event.id || t[3] === pubkey)) ||
|
||||
getContentPointers(event.content).some(
|
||||
(p) => (p.type === "nevent" && p.data.id === event.id) || (p.type === "note" && p.data === event.id),
|
||||
);
|
||||
|
||||
if (isMentioned) e[NotificationTypeSymbol] = NotificationType.Mention;
|
||||
else if (isQuote) e[NotificationTypeSymbol] = NotificationType.Quote;
|
||||
else if (isReply(event)) e[NotificationTypeSymbol] = NotificationType.Reply;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
function filterEvents(events: CategorizedEvent[], pubkey: string, mute?: Mutes): CategorizedEvent[] {
|
||||
return events.filter((event) => {
|
||||
// ignore if muted
|
||||
if (mute?.pubkeys.has(event.pubkey)) return false;
|
||||
|
||||
// ignore if own
|
||||
if (event.pubkey === pubkey) return false;
|
||||
|
||||
const e = event as CategorizedEvent;
|
||||
|
||||
switch (e[NotificationTypeSymbol]) {
|
||||
case NotificationType.Reply:
|
||||
const refs = getThreadReferences(e);
|
||||
if (!refs.reply?.e?.id) return false;
|
||||
if (refs.reply?.e?.author && refs.reply?.e?.author !== pubkey) return false;
|
||||
const parent = eventStore.getEvent(refs.reply.e.id);
|
||||
if (!parent || parent.pubkey !== pubkey) return false;
|
||||
break;
|
||||
case NotificationType.Mention:
|
||||
break;
|
||||
case NotificationType.Repost: {
|
||||
const pointer = nip18.getRepostedEventPointer(e);
|
||||
if (pointer?.author !== pubkey) return false;
|
||||
break;
|
||||
}
|
||||
case NotificationType.Reaction: {
|
||||
const pointer = nip25.getReactedEventPointer(e);
|
||||
if (!pointer) return false;
|
||||
if (pointer.author !== pubkey) return false;
|
||||
if (pointer.kind === kinds.EncryptedDirectMessage) return false;
|
||||
const parent = eventStore.getEvent(pointer.id);
|
||||
if (parent && parent.kind === kinds.EncryptedDirectMessage) return false;
|
||||
break;
|
||||
}
|
||||
case NotificationType.Zap:
|
||||
const p = getZapPayment(e);
|
||||
if (!p || p.amount === 0) return false;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function handleTextNote(event: NostrEvent) {
|
||||
// request quotes
|
||||
@ -43,10 +144,12 @@ async function handleShare(event: NostrEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
const notifications = combineLatest([accounts.active$]).pipe(
|
||||
const notifications$: Observable<CategorizedEvent[]> = combineLatest([accounts.active$]).pipe(
|
||||
mergeMap(([account]) => {
|
||||
if (account)
|
||||
return queryStore.createQuery(TimelineQuery, {
|
||||
if (!account) return [];
|
||||
|
||||
const timeline$ = queryStore
|
||||
.createQuery(TimelineQuery, {
|
||||
"#p": [account.pubkey],
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
@ -57,23 +160,34 @@ const notifications = combineLatest([accounts.active$]).pipe(
|
||||
TORRENT_COMMENT_KIND,
|
||||
kinds.LongFormArticle,
|
||||
kinds.EncryptedDirectMessage,
|
||||
1111, //NIP-22
|
||||
COMMENT_KIND,
|
||||
],
|
||||
});
|
||||
else return [];
|
||||
}),
|
||||
tap((timeline) => {
|
||||
// handle loading dependencies of each event
|
||||
for (const event of timeline) {
|
||||
switch (event.kind) {
|
||||
case kinds.ShortTextNote:
|
||||
handleTextNote(event);
|
||||
break;
|
||||
case kinds.Report:
|
||||
case kinds.GenericRepost:
|
||||
handleShare(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.pipe(
|
||||
tap((timeline) => {
|
||||
// handle loading dependencies of each event
|
||||
for (const event of timeline) {
|
||||
switch (event.kind) {
|
||||
case kinds.ShortTextNote:
|
||||
handleTextNote(event);
|
||||
break;
|
||||
case kinds.Report:
|
||||
case kinds.GenericRepost:
|
||||
handleShare(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
map((timeline) => timeline.map((e) => categorizeEvent(e, account.pubkey))),
|
||||
);
|
||||
|
||||
const mute$ = queryStore.createQuery(UserMuteQuery, account.pubkey);
|
||||
|
||||
return combineLatest([timeline$, mute$]).pipe(
|
||||
map(([timeline, mutes]) => filterEvents(timeline, account.pubkey, mutes)),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
export default notifications$;
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import Process from "../classes/process";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
class ProcessManager {
|
||||
processes = new Set<Process>();
|
||||
|
||||
registerProcess(process: Process) {
|
||||
this.processes.add(process);
|
||||
}
|
||||
unregisterProcess(process: Process) {
|
||||
this.processes.delete(process);
|
||||
for (const child of process.children) {
|
||||
this.unregisterProcess(child);
|
||||
}
|
||||
}
|
||||
|
||||
getRootProcesses() {
|
||||
return Array.from(this.processes).filter((process) => !process.parent);
|
||||
}
|
||||
getProcessRoot(process: Process): Process {
|
||||
if (process.parent) return this.getProcessRoot(process.parent);
|
||||
else return process;
|
||||
}
|
||||
getRootProcessesForRelay(relayOrUrl: string | URL | AbstractRelay) {
|
||||
const relay = relayPoolService.getRelay(relayOrUrl);
|
||||
if (!relay) return new Set<Process>();
|
||||
|
||||
const rootProcesses = new Set<Process>();
|
||||
for (const process of this.processes) {
|
||||
if (process.relays.has(relay)) {
|
||||
rootProcesses.add(this.getProcessRoot(process));
|
||||
}
|
||||
}
|
||||
|
||||
return rootProcesses;
|
||||
}
|
||||
}
|
||||
|
||||
const processManager = new ProcessManager();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-expect-error debug
|
||||
window.processManager = processManager;
|
||||
}
|
||||
|
||||
export default processManager;
|
@ -1,12 +1,10 @@
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { ReplaceableLoader } from "applesauce-loaders/loaders";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { truncateId } from "../helpers/string";
|
||||
import { eventStore } from "./event-store";
|
||||
import rxNostr from "./rx-nostr";
|
||||
import { COMMON_CONTACT_RELAYS } from "../const";
|
||||
import { getCacheRelay } from "./cache-relay";
|
||||
import { cacheRequest } from "./cache-relay";
|
||||
|
||||
export type RequestOptions = {
|
||||
/** Always request the event from the relays */
|
||||
@ -19,23 +17,6 @@ export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: str
|
||||
return `${kind}:${truncateId(pubkey)}${d ? ":" + d : ""}`;
|
||||
}
|
||||
|
||||
// load events from cache relay
|
||||
export function cacheRequest(filters: Filter[]) {
|
||||
return new Observable<NostrEvent>((observer) => {
|
||||
const relay = getCacheRelay();
|
||||
if (!relay) return observer.complete();
|
||||
|
||||
const sub = relay.subscribe(filters, {
|
||||
onevent: (event) => observer.next(event),
|
||||
oneose: () => {
|
||||
sub.close();
|
||||
observer.complete();
|
||||
},
|
||||
onclose: () => observer.complete(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const replaceableEventLoader = new ReplaceableLoader(rxNostr, { cacheRequest, lookupRelays: COMMON_CONTACT_RELAYS });
|
||||
|
||||
replaceableEventLoader.subscribe((packet) => eventStore.add(packet.event, packet.from));
|
||||
|
@ -3,15 +3,12 @@ import { BehaviorSubject, combineLatest } from "rxjs";
|
||||
import { unixNow } from "applesauce-core/helpers";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { logger } from "../helpers/debug";
|
||||
import verifyEvent from "./verify-event";
|
||||
|
||||
import authenticationSigner from "./authentication-signer";
|
||||
import localSettings from "./local-settings";
|
||||
import { unique } from "../helpers/array";
|
||||
|
||||
const log = logger.extend("rx-nostr");
|
||||
|
||||
const rxNostr = createRxNostr({
|
||||
verifier: async (event) => {
|
||||
try {
|
||||
@ -40,7 +37,6 @@ rxNostr.createConnectionStateObservable().subscribe((packet) => {
|
||||
|
||||
const url = new URL(packet.from).toString();
|
||||
connections$.next({ ...connections$.value, [url]: packet.state });
|
||||
if (import.meta.env.DEV) log(packet.state, url);
|
||||
});
|
||||
|
||||
// capture all notices sent from relays
|
||||
|
@ -3,7 +3,7 @@ import { SingleEventLoader } from "applesauce-loaders";
|
||||
|
||||
import { eventStore } from "./event-store";
|
||||
import rxNostr from "./rx-nostr";
|
||||
import { cacheRequest } from "./replaceable-loader";
|
||||
import { cacheRequest } from "./cache-relay";
|
||||
|
||||
const singleEventLoader = new SingleEventLoader(rxNostr, { cacheRequest });
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { TimelessFilter, TimelineLoader } from "applesauce-loaders";
|
||||
|
||||
import rxNostr from "./rx-nostr";
|
||||
import { logger } from "../helpers/debug";
|
||||
import { cacheRequest } from "./cache-relay";
|
||||
|
||||
const MAX_CACHE = 30;
|
||||
const BATCH_LIMIT = 100;
|
||||
@ -16,7 +17,10 @@ class TimelineCacheService {
|
||||
|
||||
if (!timeline && relays.length > 0 && filters.length > 0) {
|
||||
this.log(`Creating ${key}`);
|
||||
timeline = new TimelineLoader(rxNostr, TimelineLoader.simpleFilterMap(relays, filters), { limit: BATCH_LIMIT });
|
||||
timeline = new TimelineLoader(rxNostr, TimelineLoader.simpleFilterMap(relays, filters), {
|
||||
limit: BATCH_LIMIT,
|
||||
cacheRequest,
|
||||
});
|
||||
this.timelines.set(key, timeline);
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { UserSetsLoader } from "applesauce-loaders";
|
||||
|
||||
import { eventStore } from "./event-store";
|
||||
import rxNostr from "./rx-nostr";
|
||||
import { cacheRequest } from "./replaceable-loader";
|
||||
import { cacheRequest } from "./cache-relay";
|
||||
|
||||
const userSetsLoader = new UserSetsLoader(rxNostr, { cacheRequest });
|
||||
|
||||
|
@ -90,7 +90,7 @@ export default function DMsCard({ ...props }: Omit<CardProps, "children">) {
|
||||
<Card variant="outline" {...props}>
|
||||
<CardHeader display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Heading size="lg">
|
||||
<Link as={RouterLink} to="/dm">
|
||||
<Link as={RouterLink} to="/messages">
|
||||
Messages
|
||||
</Link>
|
||||
</Heading>
|
||||
@ -103,7 +103,7 @@ export default function DMsCard({ ...props }: Omit<CardProps, "children">) {
|
||||
{conversations.slice(0, 4).map((conversation) => (
|
||||
<Conversation key={conversation.pubkeys.join("-")} conversation={conversation} />
|
||||
))}
|
||||
<Button as={RouterLink} to="/dm" flexShrink={0} variant="link" size="lg" py="4">
|
||||
<Button as={RouterLink} to="/messages" flexShrink={0} variant="link" size="lg" py="4">
|
||||
View More
|
||||
</Button>
|
||||
</CardBody>
|
@ -1,22 +1,44 @@
|
||||
import { useCallback } from "react";
|
||||
import { Button, Card, CardBody, CardHeader, CardProps, Heading, Link } from "@chakra-ui/react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { getEventUID } from "nostr-idb";
|
||||
import { useActiveAccount, useObservable } from "applesauce-react/hooks";
|
||||
import { getEventUID } from "applesauce-core/helpers";
|
||||
import { kinds, NostrEvent } from "nostr-tools";
|
||||
|
||||
import KeyboardShortcut from "../../../components/keyboard-shortcut";
|
||||
import { useNotifications } from "../../../providers/global/notifications-provider";
|
||||
import { NotificationType, NotificationTypeSymbol } from "../../../classes/notifications";
|
||||
import NotificationItem from "../../notifications/components/notification-item";
|
||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||
import notifications$, { NotificationType, NotificationTypeSymbol } from "../../../services/notifications";
|
||||
import useForwardSubscription from "../../../hooks/use-forward-subscription";
|
||||
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
|
||||
export default function NotificationsCard({ ...props }: Omit<CardProps, "children">) {
|
||||
const navigate = useNavigate();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const account = useActiveAccount();
|
||||
const mailboxes = useUserMailboxes(account?.pubkey);
|
||||
const readRelays = useReadRelays(mailboxes?.inboxes);
|
||||
useForwardSubscription(
|
||||
readRelays,
|
||||
account
|
||||
? {
|
||||
"#p": [account.pubkey],
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Repost,
|
||||
kinds.GenericRepost,
|
||||
kinds.Reaction,
|
||||
kinds.Zap,
|
||||
kinds.LongFormArticle,
|
||||
kinds.EncryptedDirectMessage,
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const events =
|
||||
useObservable(notifications?.timeline)?.filter(
|
||||
useObservable(notifications$)?.filter(
|
||||
(event) =>
|
||||
event[NotificationTypeSymbol] === NotificationType.Mention ||
|
||||
event[NotificationTypeSymbol] === NotificationType.Reply ||
|
||||
|
@ -5,7 +5,7 @@ import RequireActiveAccount from "../../components/router/require-active-account
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import FeedsCard from "./components/feeds-card";
|
||||
import SearchForm from "./components/search-form";
|
||||
import DMsCard from "./components/dms-card";
|
||||
import DMsCard from "./components/messages-card";
|
||||
import NotificationsCard from "./components/notifications-card";
|
||||
import ToolsCard from "./components/tools-card";
|
||||
import StreamsCard from "./components/streams-card";
|
||||
|
@ -5,7 +5,6 @@ import EmbeddedUnknown from "../../../components/embed-event/event-types/embedde
|
||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "../../../components/icons";
|
||||
import { CategorizedEvent, NotificationType, NotificationTypeSymbol } from "../../../classes/notifications";
|
||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import ReplyNotification from "./reply-notification";
|
||||
@ -16,6 +15,7 @@ import ZapNotification from "./zap-notificaiton";
|
||||
import UnknownNotification from "./unknown-notification";
|
||||
import MessageNotification from "./message-notification";
|
||||
import QuoteNotification from "./quote-notification";
|
||||
import { CategorizedEvent, NotificationType, NotificationTypeSymbol } from "../../../services/notifications";
|
||||
|
||||
export const ExpandableToggleButton = ({
|
||||
toggle,
|
||||
|
@ -1,22 +1,21 @@
|
||||
import { memo, ReactNode, useCallback, useMemo } from "react";
|
||||
import { Button, ButtonGroup, Divider, Flex, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Button, Divider, Flex, Text } from "@chakra-ui/react";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { getEventUID } from "nostr-idb";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
import { useActiveAccount, useObservable } from "applesauce-react/hooks";
|
||||
import { COMMENT_KIND } from "applesauce-core/helpers";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
import RequireActiveAccount from "../../components/router/require-active-account";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { useNotifications } from "../../providers/global/notifications-provider";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import NotificationItem from "./components/notification-item";
|
||||
import NotificationTypeToggles from "./notification-type-toggles";
|
||||
import useLocalStorageDisclosure from "../../hooks/use-localstorage-disclosure";
|
||||
import { CategorizedEvent, NotificationType, NotificationTypeSymbol } from "../../classes/notifications";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import FocusedContext from "./focused-context";
|
||||
import readStatusService from "../../services/read-status";
|
||||
@ -25,6 +24,15 @@ import useNumberCache from "../../hooks/timeline/use-number-cache";
|
||||
import { useTimelineDates } from "../../hooks/timeline/use-timeline-dates";
|
||||
import useCacheEntryHeight from "../../hooks/timeline/use-cache-entry-height";
|
||||
import useVimNavigation from "./use-vim-navigation";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { truncateId } from "../../helpers/string";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||
import notifications$, {
|
||||
CategorizedEvent,
|
||||
NotificationType,
|
||||
NotificationTypeSymbol,
|
||||
} from "../../services/notifications";
|
||||
|
||||
function TimeMarker({ date, ids }: { date: Dayjs; ids: string[] }) {
|
||||
const readAll = useCallback(() => {
|
||||
@ -59,23 +67,22 @@ const NotificationsTimeline = memo(
|
||||
showReactions: boolean;
|
||||
showUnknown: boolean;
|
||||
}) => {
|
||||
const { notifications } = useNotifications();
|
||||
const { people } = usePeopleListContext();
|
||||
const peoplePubkeys = useMemo(() => people?.map((p) => p.pubkey), [people]);
|
||||
|
||||
const events = useObservable(notifications?.timeline) ?? [];
|
||||
const timeline = useObservable(notifications$) ?? [];
|
||||
|
||||
const cacheKey = useTimelineLocationCacheKey();
|
||||
const numberCache = useNumberCache(cacheKey);
|
||||
|
||||
const minItems = Math.round(window.innerHeight / 48) * 2;
|
||||
const dates = useTimelineDates(events, numberCache, minItems / 2, minItems);
|
||||
const dates = useTimelineDates(timeline, numberCache, minItems / 2, minItems);
|
||||
|
||||
// measure and cache the hight of every entry
|
||||
useCacheEntryHeight(numberCache.set);
|
||||
|
||||
const filtered: CategorizedEvent[] = [];
|
||||
for (const event of events) {
|
||||
for (const event of timeline) {
|
||||
if (event.created_at < dates.cursor && filtered.length > minItems) continue;
|
||||
|
||||
const type = event[NotificationTypeSymbol];
|
||||
@ -153,7 +160,28 @@ const NotificationsTimeline = memo(
|
||||
const cachedFocus = new BehaviorSubject("");
|
||||
|
||||
function NotificationsPage() {
|
||||
const { timeline } = useNotifications();
|
||||
const account = useActiveAccount();
|
||||
|
||||
const mailboxes = useUserMailboxes(account?.pubkey);
|
||||
const readRelays = useReadRelays(mailboxes?.inboxes);
|
||||
const { loader } = useTimelineLoader(
|
||||
`${truncateId(account?.pubkey ?? "anon")}-notification`,
|
||||
readRelays,
|
||||
account?.pubkey
|
||||
? {
|
||||
"#p": [account.pubkey],
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Repost,
|
||||
kinds.GenericRepost,
|
||||
kinds.Reaction,
|
||||
kinds.Zap,
|
||||
kinds.LongFormArticle,
|
||||
COMMENT_KIND,
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// const { value: focused, setValue: setFocused } = useRouteStateValue("focused", "");
|
||||
// const [focused, setFocused] = useState("");
|
||||
@ -168,7 +196,7 @@ function NotificationsPage() {
|
||||
const showReactions = useLocalStorageDisclosure("notifications-show-reactions", false);
|
||||
const showUnknown = useLocalStorageDisclosure("notifications-show-unknown", false);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
@ -201,7 +229,7 @@ function NotificationsPage() {
|
||||
</FocusedContext.Provider>
|
||||
</IntersectionObserverProvider>
|
||||
|
||||
<TimelineActionAndStatus loader={timeline} />
|
||||
<TimelineActionAndStatus loader={loader} />
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { Alert, AlertIcon, Button, CloseButton, Flex, Heading, Input, Text, useI
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import webRtcRelaysService from "../../../services/webrtc-relays";
|
||||
import NostrWebRtcBroker from "../../../classes/webrtc/nostr-webrtc-broker";
|
||||
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { Flex, useInterval } from "@chakra-ui/react";
|
||||
|
||||
import ProcessBranch from "./process/process-tree";
|
||||
import processManager from "../../../services/process-manager";
|
||||
import useForceUpdate from "../../../hooks/use-force-update";
|
||||
|
||||
export default function TaskManagerProcesses() {
|
||||
const update = useForceUpdate();
|
||||
useInterval(update, 500);
|
||||
|
||||
const rootProcesses = processManager.getRootProcesses();
|
||||
|
||||
return (
|
||||
<Flex direction="column">
|
||||
{rootProcesses.map((process) => (
|
||||
<ProcessBranch key={process.id} process={process} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { QuestionIcon } from "@chakra-ui/icons";
|
||||
import { ComponentWithAs, IconProps } from "@chakra-ui/react";
|
||||
|
||||
import Process from "../../../../classes/process";
|
||||
|
||||
export default function ProcessIcon({ process, ...props }: { process: Process } & IconProps) {
|
||||
const IconComponent: ComponentWithAs<"svg", IconProps> = process.icon || QuestionIcon;
|
||||
|
||||
return <IconComponent color={process.active ? "green.500" : "gray.500"} {...props} />;
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { Flex, Text, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import ProcessIcon from "./process-icon";
|
||||
import Process from "../../../../classes/process";
|
||||
import ExpandButton from "../../../tools/event-console/expand-button";
|
||||
|
||||
export default function ProcessBranch({
|
||||
process,
|
||||
level = 0,
|
||||
filter,
|
||||
}: {
|
||||
process: Process;
|
||||
level?: number;
|
||||
filter?: (process: Process) => boolean;
|
||||
}) {
|
||||
const showChildren = useDisclosure({ defaultIsOpen: !!process.parent });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="2" p="2" alignItems="center" ml={level + "em"}>
|
||||
<ProcessIcon process={process} boxSize={6} />
|
||||
<Text as="span" isTruncated fontWeight="bold">
|
||||
{process.type}
|
||||
</Text>
|
||||
<Text as="span" color="GrayText">
|
||||
{process.id}
|
||||
</Text>
|
||||
{process.children.size > 0 && (
|
||||
<ExpandButton isOpen={showChildren.isOpen} onToggle={showChildren.onToggle} variant="ghost" size="xs" />
|
||||
)}
|
||||
<Text fontSize="sm" color="GrayText">
|
||||
{process.name}
|
||||
{process.relays.size > 1
|
||||
? ` ${process.relays.size} relays`
|
||||
: Array.from(process.relays)
|
||||
.map((r) => r.url)
|
||||
.join(", ")}
|
||||
</Text>
|
||||
</Flex>
|
||||
{showChildren.isOpen &&
|
||||
Array.from(process.children)
|
||||
.filter((p) => (filter ? filter(p) : true))
|
||||
.map((child) => <ProcessBranch key={child.id} process={child} level={level + 1} filter={filter} />)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -4,7 +4,6 @@ import { Navigate, useParams } from "react-router-dom";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
|
||||
import processManager from "../../../services/process-manager";
|
||||
import { RelayAuthIconButton } from "../../../components/relays/relay-auth-icon-button";
|
||||
import RelayStatusBadge from "../../../components/relays/relay-status";
|
||||
import useRelayNotices from "../../../hooks/use-relay-notices";
|
||||
@ -14,7 +13,6 @@ export default function InspectRelayView() {
|
||||
const { relay } = useParams();
|
||||
if (!relay) return <Navigate to="/" />;
|
||||
|
||||
const rootProcesses = processManager.getRootProcessesForRelay(relay);
|
||||
const notices = useRelayNotices(relay);
|
||||
|
||||
return (
|
||||
|
@ -29,7 +29,7 @@ export default function RelayConnectionsTab() {
|
||||
<Flex direction="column">
|
||||
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
|
||||
{Object.entries(connections)
|
||||
.sort((a, b) => getConnectionStateSort(a[1]) - getConnectionStateSort(b[1]))
|
||||
.sort((a, b) => getConnectionStateSort(a[1]) - getConnectionStateSort(b[1]) || a[0].localeCompare(b[0]))
|
||||
.map(([relay]) => (
|
||||
<RelayCard key={relay} relay={relay} />
|
||||
))}
|
||||
|
Loading…
x
Reference in New Issue
Block a user