load timelines from cache relay

remove process class
This commit is contained in:
hzrd149 2025-02-03 16:08:10 -06:00
parent 1167dbae54
commit 6f933ad3e1
41 changed files with 347 additions and 702 deletions

100
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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`);

View File

@ -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>

View File

@ -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>
);

View File

@ -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(() => {

View File

@ -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);

View File

@ -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];

View File

@ -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);

View File

@ -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(",")]);

View File

@ -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 };
}

View File

@ -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>

View File

@ -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>;
}

View File

@ -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]);

View File

@ -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) => {

View File

@ -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

View File

@ -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;

View File

@ -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);
}
});

View File

@ -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;

View File

@ -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$;

View File

@ -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;

View File

@ -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));

View File

@ -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

View File

@ -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 });

View File

@ -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);
}

View File

@ -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 });

View File

@ -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>

View File

@ -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 ||

View File

@ -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";

View File

@ -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,

View File

@ -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>
);
}

View File

@ -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";

View File

@ -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>
);
}

View File

@ -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} />;
}

View File

@ -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} />)}
</>
);
}

View File

@ -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 (

View File

@ -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} />
))}