diff --git a/.changeset/purple-poets-develop.md b/.changeset/purple-poets-develop.md
new file mode 100644
index 000000000..3e34394ec
--- /dev/null
+++ b/.changeset/purple-poets-develop.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Cleanup zap parsing
diff --git a/.changeset/warm-dancers-peel.md b/.changeset/warm-dancers-peel.md
new file mode 100644
index 000000000..a889234d8
--- /dev/null
+++ b/.changeset/warm-dancers-peel.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Remove old community trending view
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fc9c008c6..2e444e572 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -95,13 +95,13 @@ importers:
version: 4.9.2(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
applesauce-channel:
specifier: ^0.8.0
- version: 0.8.0(typescript@5.6.2)
+ version: link:../applesauce/packages/channel
applesauce-content:
specifier: ^0.8.0
- version: 0.8.0(typescript@5.6.2)
+ version: link:../applesauce/packages/content
applesauce-core:
specifier: ^0.8.0
- version: 0.8.0(typescript@5.6.2)
+ version: link:../applesauce/packages/core
applesauce-lists:
specifier: ^0.8.0
version: 0.8.0(typescript@5.6.2)
@@ -110,7 +110,7 @@ importers:
version: 0.8.0(typescript@5.6.2)
applesauce-react:
specifier: ^0.8.0
- version: 0.8.0(typescript@5.6.2)
+ version: link:../applesauce/packages/react
applesauce-signer:
specifier: ^0.8.0
version: 0.8.0(typescript@5.6.2)
@@ -2310,12 +2310,6 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
- applesauce-channel@0.8.0:
- resolution: {integrity: sha512-WkE7A4WHGUdjCTwzfnfMms0KG2I62GN020wKNVIkvtdZAW1ummkSVVdcSPzZHSQZIabMRcJ5xv3tGf6EZ7tjxw==}
-
- applesauce-content@0.8.0:
- resolution: {integrity: sha512-AtEeClCbZUy5lLppjiSGQRmZN86Xsv311yYom1gCr5hQmz+vsqKdqiWukMrlyODq3g7zkNoWaN3ZRt7RNQ35sw==}
-
applesauce-core@0.8.0:
resolution: {integrity: sha512-ezH0ufSZMSS4EZlEcKSciLeq9bY2vv07pLBWLUrwZX3uCij3hcR8UtN7W78+XdWGNs0PIpSUPiWu3sEvVWFtFA==}
@@ -2325,9 +2319,6 @@ packages:
applesauce-net@0.8.0:
resolution: {integrity: sha512-yT9q6Z2slFlUP52/IqR4jWfOOfrSulCUnH3svSqwtmNGEupBEkdvMD64fbKqTGKWhyT3KkydbfTiOFDzMWv4/w==}
- applesauce-react@0.8.0:
- resolution: {integrity: sha512-ViLRORwJ5WeLshYlmPYq6t6IYST4ieuLeRu08ssR1DS3ihBFS+hjmDEht7WyR5qSGbkqqtMyqSBdYxp4mEUF0Q==}
-
applesauce-signer@0.8.0:
resolution: {integrity: sha512-TunYBkFDXA4Kc6kOhuI0zCJ/+CCGzb98jZ+RXojvVmKxOb1/JJwf103R1mY6223EqhMJtvbwrRQE66n2CdPxCw==}
@@ -3386,9 +3377,6 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
- linkifyjs@4.1.3:
- resolution: {integrity: sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==}
-
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -4145,9 +4133,6 @@ packages:
remark-wiki-link@2.0.1:
resolution: {integrity: sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA==}
- remark@15.0.1:
- resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==}
-
remove-accents@0.5.0:
resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==}
@@ -7265,33 +7250,6 @@ snapshots:
dependencies:
color-convert: 2.0.1
- applesauce-channel@0.8.0(typescript@5.6.2):
- dependencies:
- applesauce-core: 0.8.0(typescript@5.6.2)
- nostr-tools: 2.7.2(typescript@5.6.2)
- rxjs: 7.8.1
- transitivePeerDependencies:
- - supports-color
- - typescript
-
- applesauce-content@0.8.0(typescript@5.6.2):
- dependencies:
- '@cashu/cashu-ts': 1.1.0
- '@types/hast': 3.0.4
- '@types/mdast': 4.0.4
- '@types/unist': 3.0.3
- applesauce-core: 0.8.0(typescript@5.6.2)
- linkifyjs: 4.1.3
- mdast-util-find-and-replace: 3.0.1
- nostr-tools: 2.7.2(typescript@5.6.2)
- remark: 15.0.1
- remark-parse: 11.0.0
- unified: 11.0.5
- unist-util-visit-parents: 6.0.1
- transitivePeerDependencies:
- - supports-color
- - typescript
-
applesauce-core@0.8.0(typescript@5.6.2):
dependencies:
debug: 4.3.7
@@ -7328,17 +7286,6 @@ snapshots:
- supports-color
- typescript
- applesauce-react@0.8.0(typescript@5.6.2):
- dependencies:
- applesauce-content: 0.8.0(typescript@5.6.2)
- applesauce-core: 0.8.0(typescript@5.6.2)
- nostr-tools: 2.7.2(typescript@5.6.2)
- react: 18.3.1
- rxjs: 7.8.1
- transitivePeerDependencies:
- - supports-color
- - typescript
-
applesauce-signer@0.8.0(typescript@5.6.2):
dependencies:
'@noble/hashes': 1.5.0
@@ -8521,8 +8468,6 @@ snapshots:
lines-and-columns@1.2.4: {}
- linkifyjs@4.1.3: {}
-
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -9549,15 +9494,6 @@ snapshots:
mdast-util-wiki-link: 0.1.2
micromark-extension-wiki-link: 0.0.4
- remark@15.0.1:
- dependencies:
- '@types/mdast': 4.0.4
- remark-parse: 11.0.0
- remark-stringify: 11.0.0
- unified: 11.0.5
- transitivePeerDependencies:
- - supports-color
-
remove-accents@0.5.0: {}
repeat-string@1.6.1: {}
diff --git a/src/app.tsx b/src/app.tsx
index 4eee2caad..23f91721e 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -73,7 +73,6 @@ const CommunityFindByNameView = lazy(() => import("./views/community/find-by-nam
const CommunityView = lazy(() => import("./views/community/index"));
const CommunityPendingView = lazy(() => import("./views/community/views/pending"));
const CommunityNewestView = lazy(() => import("./views/community/views/newest"));
-const CommunityTrendingView = lazy(() => import("./views/community/views/trending"));
import RelaysView from "./views/relays";
import RelayView from "./views/relays/relay";
@@ -437,7 +436,6 @@ const router = createHashRouter([
element: ,
children: [
{ path: "", element: },
- { path: "trending", element: },
{ path: "newest", element: },
{ path: "pending", element: },
],
diff --git a/src/classes/batch-identifier-loader.ts b/src/classes/batch-identifier-loader.ts
index 506ef9fed..851f13192 100644
--- a/src/classes/batch-identifier-loader.ts
+++ b/src/classes/batch-identifier-loader.ts
@@ -4,6 +4,7 @@ import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import { EventStore } from "applesauce-core";
import { getEventUID } from "applesauce-core/helpers";
+import { Subject } from "rxjs";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
@@ -11,7 +12,6 @@ import processManager from "../services/process-manager";
import createDefer, { Deferred } from "./deferred";
import Dataflow04 from "../components/icons/dataflow-04";
import SuperMap from "./super-map";
-import Subject from "./subject";
/** Batches requests for events with #d tags from a single relay */
export default class BatchIdentifierLoader {
diff --git a/src/classes/batch-relation-loader.ts b/src/classes/batch-relation-loader.ts
index 53b20c30f..37e19fc4f 100644
--- a/src/classes/batch-relation-loader.ts
+++ b/src/classes/batch-relation-loader.ts
@@ -2,6 +2,7 @@ import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
+import { Subject } from "rxjs";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
@@ -9,7 +10,6 @@ import processManager from "../services/process-manager";
import createDefer, { Deferred } from "./deferred";
import Dataflow04 from "../components/icons/dataflow-04";
import SuperMap from "./super-map";
-import Subject from "./subject";
import { eventStore } from "../services/event-store";
/** Batches requests for events that reference another event (via #e tag) from a single relay */
diff --git a/src/classes/chunked-request.ts b/src/classes/chunked-request.ts
index 362e19b14..66d1038b7 100644
--- a/src/classes/chunked-request.ts
+++ b/src/classes/chunked-request.ts
@@ -4,8 +4,8 @@ import { AbstractRelay } from "nostr-tools/abstract-relay";
import { SimpleRelay } from "nostr-idb";
import _throttle from "lodash.throttle";
import { nanoid } from "nanoid";
+import { Subject } from "rxjs";
-import Subject from "./subject";
import { logger } from "../helpers/debug";
import EventStore from "./event-store";
import deleteEventService from "../services/delete-events";
diff --git a/src/classes/event-store.ts b/src/classes/event-store.ts
index f77459c4b..6822e7e76 100644
--- a/src/classes/event-store.ts
+++ b/src/classes/event-store.ts
@@ -108,7 +108,9 @@ export default class EventStore {
while (true) {
const event = events.pop();
if (!event) return;
- if (filter && !filter(event)) continue;
+ try {
+ if (filter && !filter(event)) continue;
+ } catch (error) {}
if (i === nth) return event;
i++;
}
diff --git a/src/classes/local-settings/entry.ts b/src/classes/local-settings/entry.ts
index 8e9efcfbb..52c751fb0 100644
--- a/src/classes/local-settings/entry.ts
+++ b/src/classes/local-settings/entry.ts
@@ -1,6 +1,6 @@
-import { PersistentSubject } from "../subject";
+import { BehaviorSubject } from "rxjs";
-export class NullableLocalStorageEntry extends PersistentSubject {
+export class NullableLocalStorageEntry extends BehaviorSubject {
key: string;
decode?: (raw: string | null) => T | null;
encode?: (value: T) => string | null;
@@ -44,7 +44,7 @@ export class NullableLocalStorageEntry extends PersistentSubject extends PersistentSubject {
+export class LocalStorageEntry extends BehaviorSubject {
key: string;
fallback: T;
decode?: (raw: string) => T;
diff --git a/src/classes/nostr-publish-action.ts b/src/classes/nostr-publish-action.ts
index c31902598..4946d18c8 100644
--- a/src/classes/nostr-publish-action.ts
+++ b/src/classes/nostr-publish-action.ts
@@ -4,8 +4,8 @@ import { AbstractRelay } from "nostr-tools/abstract-relay";
import relayPoolService from "../services/relay-pool";
import createDefer from "./deferred";
-import { PersistentSubject } from "./subject";
import ControlledObservable from "./controlled-observable";
+import { BehaviorSubject } from "rxjs";
export type PublishResult = { relay: AbstractRelay; success: boolean; message: string };
@@ -15,7 +15,7 @@ export default class PublishAction {
relays: string[];
event: NostrEvent;
- results = new PersistentSubject([]);
+ results = new BehaviorSubject([]);
completePromise = createDefer();
/** @deprecated */
diff --git a/src/classes/notifications.ts b/src/classes/notifications.ts
index abb06bcef..031fb67dc 100644
--- a/src/classes/notifications.ts
+++ b/src/classes/notifications.ts
@@ -2,9 +2,9 @@ import { NostrEvent, kinds, nip18, nip25 } from "nostr-tools";
import _throttle from "lodash.throttle";
import { BehaviorSubject } from "rxjs";
import { map, throttleTime } from "rxjs/operators";
+import { getZapPayment } from "applesauce-core/helpers";
import { getThreadReferences, isReply, isRepost } from "../helpers/nostr/event";
-import { getParsedZap } from "../helpers/nostr/zaps";
import singleEventService from "../services/single-event";
import RelaySet from "./relay-set";
import clientRelaysService from "../services/client-relays";
@@ -155,9 +155,8 @@ export default class AccountNotifications {
break;
}
case NotificationType.Zap:
- const parsed = getParsedZap(e, true, true);
- if (parsed instanceof Error) return false;
- if (!parsed.payment.amount) return false;
+ const p = getZapPayment(e);
+ if (!p || p.amount === 0) return false;
break;
}
diff --git a/src/classes/relay-pool.ts b/src/classes/relay-pool.ts
index 1d46d6043..5dfc82ac4 100644
--- a/src/classes/relay-pool.ts
+++ b/src/classes/relay-pool.ts
@@ -1,10 +1,10 @@
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { IConnectionPool } from "applesauce-net/connection";
import dayjs from "dayjs";
+import { Subject, BehaviorSubject } from "rxjs";
import { logger } from "../helpers/debug";
import { safeRelayUrl, validateRelayURL } from "../helpers/relay";
-import Subject, { PersistentSubject } from "./subject";
import SuperMap from "./super-map";
import verifyEventMethod from "../services/verify-event";
import { offlineMode } from "../services/offline-mode";
@@ -28,16 +28,22 @@ export default class RelayPool implements IConnectionPool {
onRelayCreated = new Subject();
onRelayChallenge = new Subject<[AbstractRelay, string]>();
- notices = new SuperMap>(() => new PersistentSubject([]));
+ notices = new SuperMap>(() => new BehaviorSubject([]));
connectionErrors = new SuperMap(() => []);
- connecting = new SuperMap>(() => new PersistentSubject(false));
+ connecting = new SuperMap>(() => new BehaviorSubject(false));
- challenges = new SuperMap>(() => new Subject());
- authForPublish = new SuperMap>(() => new Subject());
- authForSubscribe = new SuperMap>(() => new Subject());
+ challenges = new SuperMap>(
+ () => new BehaviorSubject(undefined),
+ );
+ authForPublish = new SuperMap>(
+ () => new BehaviorSubject(undefined),
+ );
+ authForSubscribe = new SuperMap>(
+ () => new BehaviorSubject(undefined),
+ );
- authenticated = new SuperMap>(() => new Subject());
+ authenticated = new SuperMap>(() => new BehaviorSubject(false));
getRelay(relayOrUrl: string | URL | AbstractRelay) {
if (typeof relayOrUrl === "string") {
diff --git a/src/classes/subject.ts b/src/classes/subject.ts
deleted file mode 100644
index 34a1679ad..000000000
--- a/src/classes/subject.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import Observable from "zen-observable";
-import { nanoid } from "nanoid";
-
-import ControlledObservable from "./controlled-observable";
-
-/** @deprecated use BehaviorSubject instead */
-export default class Subject {
- private observable: ControlledObservable;
- id = nanoid(8);
- value: T | undefined;
-
- constructor(value?: T) {
- this.observable = new ControlledObservable();
-
- this.value = value;
- this.subscribe = this.observable.subscribe.bind(this.observable);
- }
-
- next(v: T) {
- this.value = v;
- this.observable.next(v);
- }
- error(err: any) {
- this.observable.error(err);
- }
-
- [Symbol.observable]() {
- return this.observable;
- }
- subscribe: Observable["subscribe"];
-
- once(next: (value: T) => void) {
- const sub = this.subscribe((v) => {
- if (v !== undefined) {
- next(v);
- sub.unsubscribe();
- }
- });
- return sub;
- }
-
- map(callback: (value: T) => R, defaultValue?: R): Subject {
- const child = new Subject(defaultValue);
-
- if (this.value !== undefined) {
- try {
- child.next(callback(this.value));
- } catch (e) {
- child.error(e);
- }
- }
-
- this.subscribe((value) => {
- try {
- child.next(callback(value));
- } catch (e) {
- child.error(e);
- }
- });
-
- return child;
- }
-
- /** @deprecated */
- connectWithMapper(
- subject: Subject,
- map: (value: R, next: (value: T) => void, current: T | undefined) => void,
- ): ZenObservable.Subscription {
- return subject.subscribe((value) => {
- map(value, (v) => this.next(v), this.value);
- });
- }
-}
-
-export class PersistentSubject extends Subject {
- value: T;
- constructor(value: T) {
- super();
- this.value = value;
- }
-}
diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts
index 0b0568f13..02b996a1b 100644
--- a/src/classes/timeline-loader.ts
+++ b/src/classes/timeline-loader.ts
@@ -3,11 +3,10 @@ import { Debugger } from "debug";
import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import _throttle from "lodash.throttle";
-import { Observable, map } from "rxjs";
+import { BehaviorSubject, Observable, map } from "rxjs";
import { isFilterEqual } from "applesauce-core/helpers";
import { MultiSubscription } from "applesauce-net/subscription";
-import { PersistentSubject } from "./subject";
import { logger } from "../helpers/debug";
import { isReplaceable } from "../helpers/nostr/event";
import replaceableEventsService from "../services/replaceable-events";
@@ -30,8 +29,8 @@ export default class TimelineLoader {
filters: Filter[] = [];
relays: AbstractRelay[] = [];
- loading = new PersistentSubject(false);
- complete = new PersistentSubject(false);
+ loading = new BehaviorSubject(false);
+ complete = new BehaviorSubject(false);
loadNextBlockBuffer = 2;
eventFilter?: EventFilter;
@@ -66,7 +65,16 @@ export default class TimelineLoader {
if (this.eventFilter) {
// add filter
- this.timeline = this.timeline.pipe(map((events) => events.filter((e) => this.eventFilter!(e))));
+ this.timeline = this.timeline.pipe(
+ map((events) =>
+ events.filter((e) => {
+ try {
+ return this.eventFilter!(e);
+ } catch (error) {}
+ return false;
+ }),
+ ),
+ );
}
}
diff --git a/src/components/common-menu-items/quote-event.tsx b/src/components/common-menu-items/quote-event.tsx
index da9210f3e..5de3f241d 100644
--- a/src/components/common-menu-items/quote-event.tsx
+++ b/src/components/common-menu-items/quote-event.tsx
@@ -1,5 +1,6 @@
import { useCallback, useContext, useMemo } from "react";
import { MenuItem, useToast } from "@chakra-ui/react";
+import { getZapSender } from "applesauce-core/helpers";
import { kinds, nip19 } from "nostr-tools";
import { NostrEvent } from "../../types/nostr-event";
@@ -7,7 +8,6 @@ import { QuoteEventIcon } from "../icons";
import useUserProfile from "../../hooks/use-user-profile";
import { PostModalContext } from "../../providers/route/post-modal-provider";
import { getSharableEventAddress } from "../../services/event-relay-hint";
-import { getParsedZap } from "../../helpers/nostr/zaps";
export default function QuoteEventMenuItem({ event }: { event: NostrEvent }) {
const toast = useToast();
@@ -20,8 +20,8 @@ export default function QuoteEventMenuItem({ event }: { event: NostrEvent }) {
// if its a zap, mention the original author
if (event.kind === kinds.Zap) {
- const parsed = getParsedZap(event);
- if (parsed) content += "nostr:" + nip19.npubEncode(parsed.event.pubkey) + "\n";
+ const sender = getZapSender(event);
+ content += "nostr:" + nip19.npubEncode(sender) + "\n";
}
content += "\nnostr:" + address;
diff --git a/src/components/debug-modal/event-debug-modal.tsx b/src/components/debug-modal/event-debug-modal.tsx
index a3235ad56..d16a6941c 100644
--- a/src/components/debug-modal/event-debug-modal.tsx
+++ b/src/components/debug-modal/event-debug-modal.tsx
@@ -35,6 +35,7 @@ import { usePublishEvent } from "../../providers/global/publish-provider";
import { EditIcon } from "../icons";
import { RelayFavicon } from "../relay-favicon";
import { Root } from "applesauce-content/nast";
+import { ErrorBoundary } from "../error-boundary";
function Section({
label,
@@ -145,7 +146,9 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
-
+
+
+
Tags referenced in content
diff --git a/src/components/embed-event/event-types/embedded-note.tsx b/src/components/embed-event/event-types/embedded-note.tsx
index b76bd0425..c418a706a 100644
--- a/src/components/embed-event/event-types/embedded-note.tsx
+++ b/src/components/embed-event/event-types/embedded-note.tsx
@@ -1,11 +1,11 @@
import { MouseEventHandler, useCallback } from "react";
import { Card, CardProps, Flex, LinkBox, Spacer } from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
+import { useObservable } from "applesauce-react/hooks";
import { NostrEvent } from "../../../types/nostr-event";
import UserAvatarLink from "../../user/user-avatar-link";
import UserLink from "../../user/user-link";
-import useSubject from "../../../hooks/use-subject";
import EventVerificationIcon from "../../common-event/event-verification-icon";
import { TrustProvider } from "../../../providers/local/trust-provider";
import { NoteLink } from "../../note/note-link";
@@ -20,7 +20,7 @@ import useAppSettings from "../../../hooks/use-app-settings";
export default function EmbeddedNote({ event, ...props }: Omit & { event: NostrEvent }) {
const { showSignatureVerification } = useAppSettings();
- const enableDrawer = useSubject(localSettings.enableNoteThreadDrawer);
+ const enableDrawer = useObservable(localSettings.enableNoteThreadDrawer);
const navigate = enableDrawer ? useNavigateInDrawer() : useNavigate();
const to = `/n/${getSharableEventAddress(event)}`;
diff --git a/src/components/embed-event/event-types/embedded-torrent.tsx b/src/components/embed-event/event-types/embedded-torrent.tsx
index 1913f6641..874d2bc0b 100644
--- a/src/components/embed-event/event-types/embedded-torrent.tsx
+++ b/src/components/embed-event/event-types/embedded-torrent.tsx
@@ -15,6 +15,7 @@ import {
Text,
} from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
+import { useObservable } from "applesauce-react/hooks";
import UserAvatarLink from "../../user/user-avatar-link";
import UserLink from "../../user/user-link";
@@ -26,11 +27,10 @@ import { formatBytes } from "../../../helpers/number";
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
import HoverLinkOverlay from "../../hover-link-overlay";
import { getSharableEventAddress } from "../../../services/event-relay-hint";
-import useSubject from "../../../hooks/use-subject";
import localSettings from "../../../services/local-settings";
export default function EmbeddedTorrent({ torrent, ...props }: Omit & { torrent: NostrEvent }) {
- const enableDrawer = useSubject(localSettings.enableNoteThreadDrawer);
+ const enableDrawer = useObservable(localSettings.enableNoteThreadDrawer);
const navigate = enableDrawer ? useNavigateInDrawer() : useNavigate();
const link = `/torrents/${getSharableEventAddress(torrent)}`;
diff --git a/src/components/embed-event/event-types/embedded-zap-receipt.tsx b/src/components/embed-event/event-types/embedded-zap-receipt.tsx
index a436a6fd1..15eae2572 100644
--- a/src/components/embed-event/event-types/embedded-zap-receipt.tsx
+++ b/src/components/embed-event/event-types/embedded-zap-receipt.tsx
@@ -1,11 +1,20 @@
import { useMemo } from "react";
import { Box, ButtonGroup, Card, CardBody, CardHeader, CardProps, LinkBox, Text } from "@chakra-ui/react";
-import { getPointerFromTag } from "applesauce-core/helpers";
+import {
+ getPointerFromTag,
+ getZapAddressPointer,
+ getZapEventPointer,
+ getZapPayment,
+ getZapRecipient,
+ getZapRequest,
+ getZapSender,
+ isAddressPointer,
+} from "applesauce-core/helpers";
+import { DecodeResult } from "nostr-tools/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import UserLink from "../../user/user-link";
import Timestamp from "../../timestamp";
-import { getParsedZap, getZapRecipient } from "../../../helpers/nostr/zaps";
import TextNoteContents from "../../note/timeline-note/text-note-contents";
import UserAvatar from "../../user/user-avatar";
import { LightningIcon } from "../../icons";
@@ -14,37 +23,43 @@ import ZapReceiptMenu from "../../zap/zap-receipt-menu";
import { EmbedEventPointer } from "../index";
export default function EmbeddedZapRecept({ zap, ...props }: Omit & { zap: NostrEvent }) {
- const parsed = useMemo(() => getParsedZap(zap), [zap]);
- if (!parsed) return null;
- const recipient = getZapRecipient(parsed.request);
- if (!recipient) return null;
+ const sender = getZapSender(zap);
+ const recipient = getZapRecipient(zap);
+ const payment = getZapPayment(zap);
+ const request = getZapRequest(zap);
+ if (!recipient || !payment) return null;
- const eTag = parsed.request.tags.find((t) => t[0] === "e" && t[1]);
- const pointer = eTag && getPointerFromTag(eTag);
+ const pointer = useMemo(() => {
+ const event = getZapEventPointer(zap);
+ if (event) return { type: "nevent", data: event } satisfies DecodeResult;
+
+ const address = getZapAddressPointer(zap);
+ if (address) return { type: "naddr", data: address } satisfies DecodeResult;
+ }, [zap]);
return (
-
-
+
+
Zapped
- {parsed.payment.amount && (
+ {payment.amount && (
<>
- {readablizeSats(parsed.payment.amount / 1000)}
+ {readablizeSats(payment.amount / 1000)}
>
)}
-
+
-
+
{pointer && }
diff --git a/src/components/keyboard-shortcut.tsx b/src/components/keyboard-shortcut.tsx
index d5e8ad143..b43e74576 100644
--- a/src/components/keyboard-shortcut.tsx
+++ b/src/components/keyboard-shortcut.tsx
@@ -1,7 +1,8 @@
-import { Code, CodeProps } from "@chakra-ui/react";
import { useRef } from "react";
+import { Code, CodeProps } from "@chakra-ui/react";
+import { useObservable } from "applesauce-react/hooks";
import { useKeyPressEvent } from "react-use";
-import useSubject from "../hooks/use-subject";
+
import localSettings from "../services/local-settings";
export default function KeyboardShortcut({
@@ -14,7 +15,7 @@ export default function KeyboardShortcut({
requireMeta?: boolean;
onPress?: (e: KeyboardEvent) => void;
} & Omit) {
- const enableKeyboardShortcuts = useSubject(localSettings.enableKeyboardShortcuts);
+ const enableKeyboardShortcuts = useObservable(localSettings.enableKeyboardShortcuts);
const ref = useRef(null);
useKeyPressEvent(
(e) => (requireMeta ? e.ctrlKey || e.metaKey : true) && e.key === letter,
diff --git a/src/components/layout/account-switcher.tsx b/src/components/layout/account-switcher.tsx
index e9819253f..a3f237b3f 100644
--- a/src/components/layout/account-switcher.tsx
+++ b/src/components/layout/account-switcher.tsx
@@ -3,7 +3,6 @@ import { useNavigate, Link as RouterLink } from "react-router-dom";
import { Box, Button, ButtonGroup, Flex, IconButton, Text, useDisclosure } from "@chakra-ui/react";
import { getDisplayName } from "../../helpers/nostr/user-metadata";
-import useSubject from "../../hooks/use-subject";
import useUserProfile from "../../hooks/use-user-profile";
import accountService from "../../services/account";
import { AddIcon, ChevronDownIcon, ChevronUpIcon } from "../icons";
diff --git a/src/components/layout/desktop-side-nav.tsx b/src/components/layout/desktop-side-nav.tsx
index 7f2bc734f..2f481b713 100644
--- a/src/components/layout/desktop-side-nav.tsx
+++ b/src/components/layout/desktop-side-nav.tsx
@@ -2,18 +2,17 @@ import { useContext } from "react";
import { Avatar, Box, Button, Flex, FlexProps, Heading, IconButton, LinkOverlay } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { css } from "@emotion/react";
+import { useObservable } from "applesauce-react/hooks";
import useCurrentAccount from "../../hooks/use-current-account";
import AccountSwitcher from "./account-switcher";
import NavItems from "./nav-items";
import { PostModalContext } from "../../providers/route/post-modal-provider";
import { WritingIcon } from "../icons";
-import useSubject from "../../hooks/use-subject";
import { offlineMode } from "../../services/offline-mode";
import WifiOff from "../icons/wifi-off";
import TaskManagerButtons from "./task-manager-buttons";
import localSettings from "../../services/local-settings";
-import { useObservable } from "applesauce-react/hooks";
const hideScrollbar = css`
-ms-overflow-style: none;
@@ -27,7 +26,7 @@ export default function DesktopSideNav(props: Omit) {
const account = useCurrentAccount();
const { openModal } = useContext(PostModalContext);
const offline = useObservable(offlineMode);
- const showBrandLogo = useSubject(localSettings.showBrandLogo);
+ const showBrandLogo = useObservable(localSettings.showBrandLogo);
return (
& {
event: NostrEvent;
@@ -21,10 +22,10 @@ export type NoteZapButtonProps = Omit & {
export default function NoteZapButton({ event, allowComment, showEventPreview, ...props }: NoteZapButtonProps) {
const account = useCurrentAccount();
const { metadata } = useUserLNURLMetadata(event.pubkey);
- const zaps = useEventZaps(getEventUID(event));
+ const zaps = useEventZaps(getEventUID(event)) ?? [];
const { isOpen, onOpen, onClose } = useDisclosure();
- const hasZapped = !!account && zaps.some((zap) => zap.request.pubkey === account.pubkey);
+ const hasZapped = !!account && zaps.some((zap) => getZapSender(zap) === account.pubkey);
const readRelays = useReadRelays();
const onZapped = () => {
diff --git a/src/components/note/timeline-note/components/zap-bubbles.tsx b/src/components/note/timeline-note/components/zap-bubbles.tsx
index 2ac21a558..d99a7f970 100644
--- a/src/components/note/timeline-note/components/zap-bubbles.tsx
+++ b/src/components/note/timeline-note/components/zap-bubbles.tsx
@@ -2,12 +2,27 @@ import { Flex, FlexProps, Tag, TagLabel } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import styled from "@emotion/styled";
+import { getZapPayment, getZapRequest } from "applesauce-core/helpers";
import useEventZaps from "../../../../hooks/use-event-zaps";
import UserAvatar from "../../../user/user-avatar";
import { readablizeSats } from "../../../../helpers/bolt11";
import { LightningIcon } from "../../../icons";
+function ZapBubble({ zap }: { zap: NostrEvent }) {
+ const request = getZapRequest(zap);
+ const payment = getZapPayment(zap);
+
+ if (!payment) return null;
+
+ return (
+
+
+ {readablizeSats((payment.amount ?? 0) / 1000)}
+
+
+ );
+}
const HiddenScrollbar = styled(Flex)`
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
@@ -19,18 +34,14 @@ const HiddenScrollbar = styled(Flex)`
export default function ZapBubbles({ event, ...props }: { event: NostrEvent } & Omit) {
const zaps = useEventZaps(getEventUID(event));
- if (zaps.length === 0) return null;
+ if (!zaps || zaps.length === 0) return null;
- const sorted = zaps.sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0));
+ const sorted = zaps.sort((a, b) => (getZapPayment(b)?.amount ?? 0) - (getZapPayment(a)?.amount ?? 0));
return (
{sorted.map((zap) => (
-
-
- {readablizeSats((zap.payment.amount ?? 0) / 1000)}
-
-
+
))}
);
diff --git a/src/components/note/timeline-note/index.tsx b/src/components/note/timeline-note/index.tsx
index 898c27b74..923960c34 100644
--- a/src/components/note/timeline-note/index.tsx
+++ b/src/components/note/timeline-note/index.tsx
@@ -16,12 +16,12 @@ import {
import { NostrEvent } from "../../../types/nostr-event";
import UserAvatarLink from "../../user/user-avatar-link";
import { Link as RouterLink } from "react-router-dom";
+import { useObservable } from "applesauce-react/hooks";
import NoteMenu from "../note-menu";
import UserLink from "../../user/user-link";
import NoteZapButton from "../note-zap-button";
import { ExpandProvider } from "../../../providers/local/expanded";
-import useSubject from "../../../hooks/use-subject";
import EventVerificationIcon from "../../common-event/event-verification-icon";
import RepostButton from "./components/repost-button";
import QuoteEventButton from "../quote-event-button";
@@ -70,7 +70,7 @@ export function TimelineNote({
}: TimelineNoteProps) {
const account = useCurrentAccount();
const { showReactions, showSignatureVerification } = useAppSettings();
- const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
+ const hideZapBubbles = useObservable(localSettings.hideZapBubbles);
const replyForm = useDisclosure();
const ref = useEventIntersectionRef(event);
diff --git a/src/components/note/timeline-note/text-note-contents.tsx b/src/components/note/timeline-note/text-note-contents.tsx
index 92da8b908..9274c6529 100644
--- a/src/components/note/timeline-note/text-note-contents.tsx
+++ b/src/components/note/timeline-note/text-note-contents.tsx
@@ -28,10 +28,9 @@ import {
import { LightboxProvider } from "../../lightbox-provider";
import MediaOwnerProvider from "../../../providers/local/media-owner-provider";
import { components } from "../../content";
-import { fedimintTokens } from "../../../helpers/fedimint";
import { nipDefinitions } from "../../content/transform/nip-notation";
-const transformers = [...defaultTransformers, galleries, nipDefinitions, fedimintTokens];
+const transformers = [...defaultTransformers, galleries, nipDefinitions];
export type TextNoteContentsProps = {
event: NostrEvent | EventTemplate;
diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx
index 783cb4617..cc2a49def 100644
--- a/src/components/post-modal/index.tsx
+++ b/src/components/post-modal/index.tsx
@@ -32,6 +32,7 @@ import dayjs from "dayjs";
import { useForm } from "react-hook-form";
import { kinds, UnsignedEvent } from "nostr-tools";
import { useThrottle } from "react-use";
+import { useObservable } from "applesauce-react/hooks";
import { ChevronDownIcon, ChevronUpIcon, UploadImageIcon } from "../icons";
import PublishAction from "../../classes/nostr-publish-action";
@@ -59,7 +60,6 @@ import useAppSettings from "../../hooks/use-app-settings";
import { ErrorBoundary } from "../error-boundary";
import { useFinalizeDraft, usePublishEvent } from "../../providers/global/publish-provider";
import { TextNoteContents } from "../note/timeline-note/text-note-contents";
-import useSubject from "../../hooks/use-subject";
import localSettings from "../../services/local-settings";
import useLocalStorageDisclosure from "../../hooks/use-localstorage-disclosure";
@@ -92,7 +92,7 @@ export default function PostModal({
const finalizeDraft = useFinalizeDraft();
const account = useCurrentAccount()!;
const { noteDifficulty } = useAppSettings();
- const addClientTag = useSubject(localSettings.addClientTag);
+ const addClientTag = useObservable(localSettings.addClientTag);
const promptAddClientTag = useLocalStorageDisclosure("prompt-add-client-tag", true);
const [miningTarget, setMiningTarget] = useState(0);
const [publishAction, setPublishAction] = useState();
diff --git a/src/components/relays/relay-auth-button.tsx b/src/components/relays/relay-auth-button.tsx
index ecb2ea879..88c26d4fb 100644
--- a/src/components/relays/relay-auth-button.tsx
+++ b/src/components/relays/relay-auth-button.tsx
@@ -1,15 +1,15 @@
import { useCallback, useState } from "react";
import { IconButton, IconButtonProps, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
import { type AbstractRelay } from "nostr-tools/abstract-relay";
+import { useObservable } from "applesauce-react/hooks";
import relayPoolService from "../../services/relay-pool";
import { useSigningContext } from "../../providers/global/signing-provider";
import PasscodeLock from "../icons/passcode-lock";
-import useSubject from "../../hooks/use-subject";
import CheckCircleBroken from "../icons/check-circle-broken";
export function useRelayChallenge(relay: AbstractRelay) {
- return useSubject(relayPoolService.challenges.get(relay));
+ return useObservable(relayPoolService.challenges.get(relay));
}
export function useRelayAuthMethod(relay: AbstractRelay) {
@@ -17,7 +17,7 @@ export function useRelayAuthMethod(relay: AbstractRelay) {
const { requestSignature } = useSigningContext();
const challenge = useRelayChallenge(relay);
- const authenticated = useSubject(relayPoolService.authenticated.get(relay));
+ const authenticated = useObservable(relayPoolService.authenticated.get(relay));
const [loading, setLoading] = useState(false);
const auth = useCallback(async () => {
diff --git a/src/components/relays/relay-connect-switch.tsx b/src/components/relays/relay-connect-switch.tsx
index c9e10ccb5..52e95e6e6 100644
--- a/src/components/relays/relay-connect-switch.tsx
+++ b/src/components/relays/relay-connect-switch.tsx
@@ -1,9 +1,9 @@
import { ChangeEventHandler } from "react";
import { Switch, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
import { type AbstractRelay } from "nostr-tools/abstract-relay";
+import { useObservable } from "applesauce-react/hooks";
import relayPoolService from "../../services/relay-pool";
-import useSubject from "../../hooks/use-subject";
export default function RelayConnectSwitch({ relay }: { relay: string | URL | AbstractRelay }) {
const toast = useToast();
@@ -14,7 +14,7 @@ export default function RelayConnectSwitch({ relay }: { relay: string | URL | Ab
const update = useForceUpdate();
useInterval(update, 500);
- const connecting = useSubject(relayPoolService.connecting.get(r));
+ const connecting = useObservable(relayPoolService.connecting.get(r));
const onChange: ChangeEventHandler = async (e) => {
try {
diff --git a/src/components/relays/relay-status.tsx b/src/components/relays/relay-status.tsx
index 2be5514d2..a169efb22 100644
--- a/src/components/relays/relay-status.tsx
+++ b/src/components/relays/relay-status.tsx
@@ -1,9 +1,9 @@
import { Badge, useForceUpdate } from "@chakra-ui/react";
import { useInterval } from "react-use";
import { AbstractRelay } from "nostr-tools/abstract-relay";
+import { useObservable } from "applesauce-react/hooks";
import relayPoolService from "../../services/relay-pool";
-import useSubject from "../../hooks/use-subject";
const getStatusText = (relay: AbstractRelay, connecting = false) => {
if (connecting) return "Connecting...";
@@ -31,7 +31,7 @@ export const RelayStatus = ({ url, relay }: { url?: string; relay?: AbstractRela
else throw Error("Missing url or relay");
}
- const connecting = useSubject(relayPoolService.connecting.get(relay!));
+ const connecting = useObservable(relayPoolService.connecting.get(relay!));
return {getStatusText(relay!, connecting)};
};
diff --git a/src/components/timeline/timeline-action-and-status.tsx b/src/components/timeline/timeline-action-and-status.tsx
index 81fef51a3..e8eecd569 100644
--- a/src/components/timeline/timeline-action-and-status.tsx
+++ b/src/components/timeline/timeline-action-and-status.tsx
@@ -1,11 +1,11 @@
import { Alert, AlertIcon, Button, Spinner } from "@chakra-ui/react";
+import { useObservable } from "applesauce-react/hooks";
import TimelineLoader from "../../classes/timeline-loader";
-import useSubject from "../../hooks/use-subject";
export default function TimelineActionAndStatus({ timeline }: { timeline: TimelineLoader }) {
- const loading = useSubject(timeline.loading);
- const complete = useSubject(timeline.complete);
+ const loading = useObservable(timeline.loading);
+ const complete = useObservable(timeline.complete);
if (complete) {
return (
diff --git a/src/components/user/user-link.tsx b/src/components/user/user-link.tsx
index 0169f13f2..39193d0e5 100644
--- a/src/components/user/user-link.tsx
+++ b/src/components/user/user-link.tsx
@@ -6,8 +6,6 @@ import { getDisplayName } from "../../helpers/nostr/user-metadata";
import useUserProfile from "../../hooks/use-user-profile";
import useAppSettings from "../../hooks/use-app-settings";
import useCurrentAccount from "../../hooks/use-current-account";
-import useSubject from "../../hooks/use-subject";
-import localSettings from "../../services/local-settings";
export type UserLinkProps = LinkProps & {
pubkey: string;
diff --git a/src/helpers/nostr/event.ts b/src/helpers/nostr/event.ts
index a989f2351..fd47fc5ba 100644
--- a/src/helpers/nostr/event.ts
+++ b/src/helpers/nostr/event.ts
@@ -2,6 +2,7 @@ import { EventTemplate, kinds, validateEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import dayjs from "dayjs";
import { nanoid } from "nanoid";
+import { getNip10References } from "applesauce-core/helpers";
import { ATag, ETag, isDTag, isETag, isPTag, NostrEvent, Tag } from "../../types/nostr-event";
import { getMatchNostrLink } from "../regexp";
@@ -11,7 +12,6 @@ import { safeDecode } from "../nip19";
import { safeRelayUrl, safeRelayUrls } from "../relay";
import RelaySet from "../../classes/relay-set";
import { truncateId } from "../string";
-import { getNip10References } from "./threading";
export { truncateId as truncatedId };
@@ -37,14 +37,18 @@ export function pointerMatchEvent(event: NostrEvent, pointer: AddressPointer | E
const isReplySymbol = Symbol("isReply");
export function isReply(event: NostrEvent | EventTemplate) {
- // @ts-expect-error
- if (event[isReplySymbol] !== undefined) return event[isReplySymbol] as boolean;
+ try {
+ // @ts-expect-error
+ if (event[isReplySymbol] !== undefined) return event[isReplySymbol] as boolean;
- if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
- const isReply = !!getNip10References(event).reply;
- // @ts-expect-error
- event[isReplySymbol] = isReply;
- return isReply;
+ if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
+ const isReply = !!getNip10References(event).reply;
+ // @ts-expect-error
+ event[isReplySymbol] = isReply;
+ return isReply;
+ } catch (error) {
+ return false;
+ }
}
export function isPTagMentionedInContent(event: NostrEvent | EventTemplate, pubkey: string) {
return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey);
diff --git a/src/helpers/nostr/threading.ts b/src/helpers/nostr/threading.ts
deleted file mode 100644
index 13169d31c..000000000
--- a/src/helpers/nostr/threading.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { EventTemplate, NostrEvent } from "nostr-tools";
-import { AddressPointer, EventPointer } from "nostr-tools/nip19";
-
-import { ATag, ETag, isATag, isETag } from "../../types/nostr-event";
-import { aTagToAddressPointer, eTagToEventPointer, getContentTagRefs } from "./event";
-
-export function interpretThreadTags(event: NostrEvent | EventTemplate) {
- const eTags = event.tags.filter(isETag);
- const aTags = event.tags.filter(isATag);
-
- // find the root and reply tags.
- let rootETag = eTags.find((t) => t[3] === "root");
- let replyETag = eTags.find((t) => t[3] === "reply");
-
- let rootATag = aTags.find((t) => t[3] === "root");
- let replyATag = aTags.find((t) => t[3] === "reply");
-
- if (!rootETag || !replyETag) {
- // a direct reply does not need a "reply" reference
- // https://github.com/nostr-protocol/nips/blob/master/10.md
-
- // this is not necessarily to spec. but if there is only one id (root or reply) then assign it to both
- // this handles the cases where a client only set a "reply" tag and no root
- rootETag = replyETag = rootETag || replyETag;
- }
- if (!rootATag || !replyATag) {
- rootATag = replyATag = rootATag || replyATag;
- }
-
- if (!rootETag && !replyETag) {
- const contentTagRefs = getContentTagRefs(event.content, eTags);
-
- // legacy behavior
- // https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
- const legacyETags = eTags.filter((t) => {
- // ignore it if there is a type
- if (t[3]) return false;
- if (contentTagRefs.includes(t)) return false;
- return true;
- });
-
- if (legacyETags.length >= 1) {
- // first tag is the root
- rootETag = legacyETags[0];
- // last tag is reply
- replyETag = legacyETags[legacyETags.length - 1] ?? rootETag;
- }
- }
-
- return {
- root: rootETag || rootATag ? { e: rootETag, a: rootATag } : undefined,
- reply: replyETag || replyATag ? { e: replyETag, a: replyATag } : undefined,
- } as {
- root?: { e: ETag; a: undefined } | { e: undefined; a: ATag } | { e: ETag; a: ATag };
- reply?: { e: ETag; a: undefined } | { e: undefined; a: ATag } | { e: ETag; a: ATag };
- };
-}
-
-export type ThreadReferences = {
- root?:
- | { e: EventPointer; a: undefined }
- | { e: undefined; a: AddressPointer }
- | { e: EventPointer; a: AddressPointer };
- reply?:
- | { e: EventPointer; a: undefined }
- | { e: undefined; a: AddressPointer }
- | { e: EventPointer; a: AddressPointer };
-};
-export const threadRefsSymbol = Symbol("threadRefs");
-export type EventWithThread = (NostrEvent | EventTemplate) & { [threadRefsSymbol]: ThreadReferences };
-
-export function getNip10References(event: NostrEvent | EventTemplate): ThreadReferences {
- // @ts-expect-error
- if (Object.hasOwn(event, threadRefsSymbol)) return event[threadRefsSymbol];
-
- const e = event as EventWithThread;
- const tags = interpretThreadTags(e);
-
- const threadRef = {
- root: tags.root && {
- e: tags.root.e && eTagToEventPointer(tags.root.e),
- a: tags.root.a && aTagToAddressPointer(tags.root.a),
- },
- reply: tags.reply && {
- e: tags.reply.e && eTagToEventPointer(tags.reply.e),
- a: tags.reply.a && aTagToAddressPointer(tags.reply.a),
- },
- } as ThreadReferences;
-
- // @ts-expect-error
- event[threadRefsSymbol] = threadRef;
-
- return threadRef;
-}
diff --git a/src/helpers/nostr/user-metadata.ts b/src/helpers/nostr/user-metadata.ts
index 522c4cddb..21403100d 100644
--- a/src/helpers/nostr/user-metadata.ts
+++ b/src/helpers/nostr/user-metadata.ts
@@ -3,6 +3,7 @@ import emojiRegex from "emoji-regex";
import { truncatedId } from "./event";
import { ProfileContent } from "applesauce-core/helpers";
+/** @deprecated use ProfileContent instead */
export type Kind0ParsedContent = {
pubkey?: string;
name?: string;
diff --git a/src/helpers/nostr/zaps.ts b/src/helpers/nostr/zaps.ts
index a5e928a07..2005018ad 100644
--- a/src/helpers/nostr/zaps.ts
+++ b/src/helpers/nostr/zaps.ts
@@ -1,13 +1,11 @@
import { bech32 } from "@scure/base";
+import {} from "nostr-tools/nip57";
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
-import { ParsedInvoice, parsePaymentRequest } from "../bolt11";
-
-import { Kind0ParsedContent } from "./user-metadata";
-import { nip57, utils } from "nostr-tools";
-import verifyEvent from "../../services/verify-event";
+import { utils } from "nostr-tools";
+import { getZapPayment, ProfileContent } from "applesauce-core/helpers";
// based on https://github.com/nbd-wtf/nostr-tools/blob/master/nip57.ts
-export async function getZapEndpoint(metadata: Kind0ParsedContent): Promise {
+export async function getZapEndpoint(metadata: ProfileContent): Promise {
try {
let lnurl: string = "";
let { lud06, lud16 } = metadata;
@@ -42,79 +40,8 @@ export function isProfileZap(event: NostrEvent) {
return !isNoteZap(event) && event.tags.some(isPTag);
}
-export function getZapRecipient(event: NostrEvent) {
- return event.tags.find((t) => t[0] === "p" && t[1])?.[1];
-}
-
-export function totalZaps(zaps: ParsedZap[]) {
- return zaps.reduce((t, zap) => t + (zap.payment.amount || 0), 0);
-}
-
-export type ParsedZap = {
- event: NostrEvent;
- request: NostrEvent;
- payment: ParsedInvoice;
- eventId?: string;
-};
-
-const parsedZapSymbol = Symbol("parsedZap");
-type ParsedZapEvent = NostrEvent & { [parsedZapSymbol]: ParsedZap | Error };
-
-export function getParsedZap(event: NostrEvent, quite: false, returnError?: boolean): ParsedZap;
-export function getParsedZap(event: NostrEvent, quite: true, returnError: true): ParsedZap | Error;
-export function getParsedZap(event: NostrEvent, quite: true, returnError: false): ParsedZap | undefined;
-export function getParsedZap(event: NostrEvent, quite?: boolean, returnError?: boolean): ParsedZap | undefined;
-export function getParsedZap(event: NostrEvent, quite: boolean = true, returnError?: boolean) {
- const e = event as ParsedZapEvent;
- if (Object.hasOwn(e, parsedZapSymbol)) {
- const cached = e[parsedZapSymbol];
- if (!returnError && cached instanceof Error) return undefined;
- if (!quite && cached instanceof Error) throw cached;
- return cached;
- }
-
- try {
- return (e[parsedZapSymbol] = parseZapEvent(e));
- } catch (error) {
- if (error instanceof Error) {
- e[parsedZapSymbol] = error;
- if (quite) return returnError ? error : undefined;
- else throw error;
- } else throw error;
- }
-}
-
-export function parseZapEvents(events: NostrEvent[]) {
- const parsed: ParsedZap[] = [];
-
- for (const event of events) {
- const p = getParsedZap(event);
- if (p) parsed.push(p);
- }
-
- return parsed;
-}
-
-/** @deprecated use getParsedZap instead */
-export function parseZapEvent(event: NostrEvent): ParsedZap {
- const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
- if (!zapRequestStr) throw new Error("No description tag");
-
- const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1];
- if (!bolt11) throw new Error("Missing bolt11 invoice");
-
- const error = nip57.validateZapRequest(zapRequestStr);
- if (error) throw new Error(error);
-
- const request = JSON.parse(zapRequestStr) as NostrEvent;
- if (!verifyEvent(request)) throw new Error("Invalid zap request");
- const payment = parsePaymentRequest(bolt11);
-
- return {
- event,
- request,
- payment,
- };
+export function totalZaps(zaps: NostrEvent[]) {
+ return zaps.map(getZapPayment).reduce((t, p) => t + (p?.amount ?? 0), 0);
}
export type EventSplit = { pubkey: string; percent: number; relay?: string }[];
diff --git a/src/hooks/use-dns-identity.ts b/src/hooks/use-dns-identity.ts
index b8672a9cc..98a94bb15 100644
--- a/src/hooks/use-dns-identity.ts
+++ b/src/hooks/use-dns-identity.ts
@@ -1,11 +1,12 @@
import { useMemo } from "react";
+import { useObservable } from "applesauce-react/hooks";
+
import dnsIdentityService from "../services/dns-identity";
-import useSubject from "./use-subject";
export default function useDnsIdentity(address: string | undefined) {
const subject = useMemo(() => {
if (address) return dnsIdentityService.getIdentity(address);
}, [address]);
- return useSubject(subject);
+ return useObservable(subject);
}
diff --git a/src/hooks/use-event-count.ts b/src/hooks/use-event-count.ts
index dc28d8392..080ce54c7 100644
--- a/src/hooks/use-event-count.ts
+++ b/src/hooks/use-event-count.ts
@@ -1,10 +1,11 @@
import { useMemo } from "react";
import { Filter } from "nostr-tools";
+import { useObservable } from "applesauce-react/hooks";
+
import eventCountService from "../services/event-count";
-import useSubject from "./use-subject";
export default function useEventCount(filter?: Filter | Filter[], alwaysRequest = false) {
const key = filter ? eventCountService.stringifyFilter(filter) : "empty";
const subject = useMemo(() => filter && eventCountService.requestCount(filter, alwaysRequest), [key, alwaysRequest]);
- return useSubject(subject);
+ return useObservable(subject);
}
diff --git a/src/hooks/use-event-reactions.ts b/src/hooks/use-event-reactions.ts
index a874d66a1..eb6d7a25f 100644
--- a/src/hooks/use-event-reactions.ts
+++ b/src/hooks/use-event-reactions.ts
@@ -1,11 +1,11 @@
+import { useEffect } from "react";
import { NostrEvent } from "nostr-tools";
import { getEventUID } from "applesauce-core/helpers";
+import { useStoreQuery } from "applesauce-react/hooks";
+import { ReactionsQuery } from "applesauce-core/queries";
import eventReactionsService from "../services/event-reactions";
import { useReadRelays } from "./use-client-relays";
-import { queryStore } from "../services/event-store";
-import { useObservable } from "./use-observable";
-import { useEffect } from "react";
export default function useEventReactions(
event: NostrEvent,
@@ -18,6 +18,5 @@ export default function useEventReactions(
eventReactionsService.requestReactions(getEventUID(event), relays, alwaysRequest);
}, [event, relays, alwaysRequest]);
- const observable = queryStore.reactions(event);
- return useObservable(observable);
+ return useStoreQuery(ReactionsQuery, [event]);
}
diff --git a/src/hooks/use-event-zaps.ts b/src/hooks/use-event-zaps.ts
index decfa4cba..fb55b98ad 100644
--- a/src/hooks/use-event-zaps.ts
+++ b/src/hooks/use-event-zaps.ts
@@ -1,28 +1,22 @@
-import { useMemo } from "react";
+import { useEffect, useMemo } from "react";
+import { useStoreQuery } from "applesauce-react/hooks";
+import { parseCoordinate } from "applesauce-core/helpers";
+import { EventZapsQuery } from "applesauce-core/queries";
import eventZapsService from "../services/event-zaps";
import { useReadRelays } from "./use-client-relays";
-import useSubject from "./use-subject";
-import { getParsedZap } from "../helpers/nostr/zaps";
-export default function useEventZaps(eventUID: string, additionalRelays?: Iterable, alwaysRequest = true) {
+export default function useEventZaps(uid: string, additionalRelays?: Iterable, alwaysRequest = false) {
const readRelays = useReadRelays(additionalRelays);
- const subject = useMemo(
- () => eventZapsService.requestZaps(eventUID, readRelays, alwaysRequest),
- [eventUID, readRelays.urls.join("|"), alwaysRequest],
- );
+ useEffect(() => {
+ eventZapsService.requestZaps(uid, readRelays, alwaysRequest);
+ }, [uid, readRelays.urls.join("|"), alwaysRequest]);
- const events = useSubject(subject) || [];
+ const pointer = useMemo(() => {
+ if (uid.includes(":")) return parseCoordinate(uid, true);
+ return uid;
+ }, [uid]);
- const zaps = useMemo(() => {
- const parsed = [];
- for (const zap of events) {
- const p = getParsedZap(zap);
- if (p) parsed.push(p);
- }
- return parsed;
- }, [events]);
-
- return zaps;
+ return useStoreQuery(EventZapsQuery, pointer ? [pointer] : undefined) ?? [];
}
diff --git a/src/hooks/use-events-reactions.ts b/src/hooks/use-events-reactions.ts
deleted file mode 100644
index 2637f6eb5..000000000
--- a/src/hooks/use-events-reactions.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useMemo, useState } from "react";
-import eventReactionsService from "../services/event-reactions";
-import { useReadRelays } from "./use-client-relays";
-import { NostrEvent } from "../types/nostr-event";
-import Subject from "../classes/subject";
-import useSubjects from "./use-subjects";
-
-export default function useEventsReactions(
- eventIds: string[],
- additionalRelays?: Iterable,
- alwaysRequest = true,
-) {
- const readRelays = useReadRelays(additionalRelays);
-
- // get subjects
- const subjects = useMemo(() => {
- const dir: Record> = {};
- for (const eventId of eventIds) {
- dir[eventId] = eventReactionsService.requestReactions(eventId, readRelays, alwaysRequest);
- }
- return dir;
- }, [eventIds, readRelays.urls.join("|"), alwaysRequest]);
-
- // get values out of subjects
- const reactions: Record = {};
- for (const [id, subject] of Object.entries(subjects)) {
- if (subject.value) reactions[id] = subject.value;
- }
-
- const [_, update] = useState(0);
-
- // subscribe to subjects
- useSubjects(Object.values(subjects));
-
- return reactions;
-}
diff --git a/src/hooks/use-kind4-decryption.ts b/src/hooks/use-kind4-decryption.ts
index 6ba499ec6..e49d9c97a 100644
--- a/src/hooks/use-kind4-decryption.ts
+++ b/src/hooks/use-kind4-decryption.ts
@@ -1,9 +1,9 @@
import { useCallback, useMemo } from "react";
import { NostrEvent } from "nostr-tools";
+import { useObservable } from "applesauce-react/hooks";
import decryptionCacheService from "../services/decryption-cache";
import useCurrentAccount from "./use-current-account";
-import useSubject from "./use-subject";
import { getDMRecipient, getDMSender } from "../helpers/nostr/dms";
export function useKind4Decrypt(event: NostrEvent, pubkey?: string) {
@@ -16,8 +16,8 @@ export function useKind4Decrypt(event: NostrEvent, pubkey?: string) {
[event, pubkey],
);
- const plaintext = useSubject(container.plaintext);
- const error = useSubject(container.error);
+ const plaintext = useObservable(container.plaintext);
+ const error = useObservable(container.error);
const requestDecrypt = useCallback(() => {
const p = decryptionCacheService.requestDecrypt(container);
diff --git a/src/hooks/use-observable.ts b/src/hooks/use-observable.ts
deleted file mode 100644
index d697f0916..000000000
--- a/src/hooks/use-observable.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { useObservable } from "applesauce-react/hooks";
-
-export { useObservable };
diff --git a/src/hooks/use-read-status.ts b/src/hooks/use-read-status.ts
index bd49cc038..57f5ee9f3 100644
--- a/src/hooks/use-read-status.ts
+++ b/src/hooks/use-read-status.ts
@@ -1,13 +1,14 @@
import { useCallback, useMemo } from "react";
+import { useObservable } from "applesauce-react/hooks";
+
import readStatusService from "../services/read-status";
-import useSubject from "./use-subject";
export default function useReadStatus(key: string, ttl?: number) {
const subject = useMemo(() => readStatusService.getStatus(key, ttl), [key]);
const setRead = useCallback((read = true) => readStatusService.setRead(key, read, ttl), [key, ttl]);
- const read = useSubject(subject);
+ const read = useObservable(subject);
return [read, setRead] as const;
}
diff --git a/src/hooks/use-relay-stats.ts b/src/hooks/use-relay-stats.ts
index 265ab5470..416b73de6 100644
--- a/src/hooks/use-relay-stats.ts
+++ b/src/hooks/use-relay-stats.ts
@@ -1,12 +1,12 @@
+import { useObservable } from "applesauce-react/hooks";
import relayStatsService from "../services/relay-stats";
-import useSubject from "./use-subject";
export default function useRelayStats(relay: string) {
const monitorSub = relayStatsService.requestMonitorStats(relay);
const selfReportedSub = relayStatsService.requestSelfReported(relay);
- const monitor = useSubject(monitorSub);
- const selfReported = useSubject(selfReportedSub);
+ const monitor = useObservable(monitorSub);
+ const selfReported = useObservable(selfReportedSub);
const stats = monitor || selfReported || undefined;
return {
diff --git a/src/hooks/use-subject.ts b/src/hooks/use-subject.ts
deleted file mode 100644
index a5e8bf98e..000000000
--- a/src/hooks/use-subject.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import Subject, { PersistentSubject } from "../classes/subject";
-
-function useSubject(subject: PersistentSubject): Value;
-function useSubject(subject?: PersistentSubject): Value | undefined;
-function useSubject(subject?: Subject): Value | undefined;
-function useSubject(subject?: Subject) {
- const [_, setValue] = useState(subject?.value);
- const subRef = useRef(subject);
- useEffect(() => {
- if (subject?.value !== undefined) setValue(subject?.value);
- const sub = subject?.subscribe((v) => setValue(v));
- return () => sub?.unsubscribe();
- }, [subject, setValue]);
-
- return subject?.value;
-}
-
-export default useSubject;
diff --git a/src/hooks/use-subjects.ts b/src/hooks/use-subjects.ts
deleted file mode 100644
index 3e5a0a2c4..000000000
--- a/src/hooks/use-subjects.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useEffect, useState } from "react";
-import Subject, { PersistentSubject } from "../classes/subject";
-
-function useSubjects(
- subjects: (Subject | PersistentSubject | undefined)[] = [],
-): Value[] {
- const values = subjects.map((sub) => sub?.value).filter((v) => v !== undefined) as Value[];
- const [_, update] = useState(0);
-
- useEffect(() => {
- const listener = () => update((v) => v + 1);
- const subs = subjects.map((s) => s?.subscribe(listener));
- return () => {
- for (const sub of subs) sub?.unsubscribe();
- };
- }, [subjects, update]);
-
- return values;
-}
-
-export default useSubjects;
diff --git a/src/hooks/use-timeline-loader.ts b/src/hooks/use-timeline-loader.ts
index 2a5e50a8f..7cc1bb9cd 100644
--- a/src/hooks/use-timeline-loader.ts
+++ b/src/hooks/use-timeline-loader.ts
@@ -3,7 +3,7 @@ import { usePrevious, useUnmount } from "react-use";
import { Filter } from "nostr-tools";
import timelineCacheService from "../services/timeline-cache";
-import TimelineLoader, { EventFilter } from "../classes/timeline-loader";
+import { EventFilter } from "../classes/timeline-loader";
import { useStoreQuery } from "applesauce-react/hooks";
import { Queries } from "applesauce-core";
@@ -57,7 +57,13 @@ export default function useTimelineLoader(
});
let timeline = useStoreQuery(Queries.TimelineQuery, filters && [filters]) ?? [];
- if (opts?.eventFilter) timeline = timeline.filter(opts.eventFilter);
+ if (opts?.eventFilter)
+ timeline = timeline.filter((e) => {
+ try {
+ return opts.eventFilter && opts.eventFilter(e);
+ } catch (error) {}
+ return false;
+ });
return { loader, timeline };
}
diff --git a/src/providers/global/publish-provider.tsx b/src/providers/global/publish-provider.tsx
index 52a637bbd..abe6c9333 100644
--- a/src/providers/global/publish-provider.tsx
+++ b/src/providers/global/publish-provider.tsx
@@ -122,7 +122,6 @@ export default function PublishProvider({ children }: PropsWithChildren) {
// pass it to other services
eventStore.add(signed);
if (isReplaceable(signed.kind)) replaceableEventsService.handleEvent(signed);
- if (signed.kind === kinds.Reaction) eventReactionsService.handleEvent(signed);
if (signed.kind === kinds.EventDeletion) deleteEventService.handleEvent(signed);
return pub;
} catch (e) {
diff --git a/src/providers/local/intersection-observer.tsx b/src/providers/local/intersection-observer.tsx
index 394a2d8be..b66b308bb 100644
--- a/src/providers/local/intersection-observer.tsx
+++ b/src/providers/local/intersection-observer.tsx
@@ -10,8 +10,7 @@ import {
useState,
} from "react";
import { useMount, useUnmount } from "react-use";
-
-import Subject from "../../classes/subject";
+import { BehaviorSubject, Subject } from "rxjs";
const IntersectionObserverContext = createContext<{
observer?: IntersectionObserver;
@@ -77,7 +76,7 @@ export default function IntersectionObserverProvider({
threshold?: IntersectionObserverInit["threshold"];
callback: IntersectionObserverCallback;
}) {
- const [subject] = useState(() => new Subject([]));
+ const [subject] = useState(() => new BehaviorSubject([]));
const handleIntersection = useCallback(
(entries, observer) => {
diff --git a/src/services/channel-metadata.ts b/src/services/channel-metadata.ts
index 5ba32b428..a6043f9f4 100644
--- a/src/services/channel-metadata.ts
+++ b/src/services/channel-metadata.ts
@@ -1,49 +1,44 @@
import dayjs from "dayjs";
-import debug, { Debugger } from "debug";
+import { Debugger } from "debug";
import _throttle from "lodash.throttle";
import { Filter, kinds } from "nostr-tools";
+import { getChannelPointer } from "applesauce-channel";
-import NostrSubscription from "../classes/nostr-subscription";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
-import Subject from "../classes/subject";
import { logger } from "../helpers/debug";
-import db from "./db";
-import createDefer, { Deferred } from "../classes/deferred";
-import { getChannelPointer } from "../helpers/nostr/channel";
import { eventStore } from "./event-store";
-
-type Pubkey = string;
-type Relay = string;
+import relayPoolService from "./relay-pool";
+import PersistentSubscription from "../classes/persistent-subscription";
+import { localRelay } from "./local-relay";
+import { isFromCache, markFromCache } from "applesauce-core/helpers";
+import { AbstractRelay } from "nostr-tools/abstract-relay";
export type RequestOptions = {
/** Always request the event from the relays */
alwaysRequest?: boolean;
/** ignore the cache on initial load */
ignoreCache?: boolean;
- // TODO: figure out a clean way for useReplaceableEvent hook to "unset" or "unsubscribe"
- // keepAlive?: boolean;
};
const RELAY_REQUEST_BATCH_TIME = 1000;
/** This class is ued to batch requests to a single relay */
class ChannelMetadataRelayLoader {
- private subscription: NostrSubscription;
- private events = new SuperMap>(() => new Subject());
+ private subscription: PersistentSubscription;
private requestNext = new Set();
private requested = new Map();
log: Debugger;
+ isCache = false;
- constructor(relay: string, log?: Debugger) {
- this.subscription = new NostrSubscription(relay, undefined, `channel-metadata-loader`);
-
- this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
- this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
-
- this.log = log || debug("misc");
+ constructor(relay: AbstractRelay, log?: Debugger) {
+ this.log = log || logger.extend("ChannelMetadataRelayLoader");
+ this.subscription = new PersistentSubscription(relay, {
+ onevent: (event) => this.handleEvent(event),
+ oneose: () => this.handleEOSE(),
+ });
}
private handleEvent(event: NostrEvent) {
@@ -53,31 +48,18 @@ class ChannelMetadataRelayLoader {
// remove the pubkey from the waiting list
this.requested.delete(channelId);
- const sub = this.events.get(channelId);
+ if (this.isCache) markFromCache(event);
- const current = sub.value;
- if (!current || event.created_at > current.created_at) {
- sub.next(event);
- }
+ eventStore.add(event);
}
private handleEOSE() {
// relays says it has nothing left
this.requested.clear();
}
- getSubject(channelId: string) {
- return this.events.get(channelId);
- }
-
requestMetadata(channelId: string) {
- const subject = this.events.get(channelId);
-
- if (!subject.value) {
- this.requestNext.add(channelId);
- this.updateThrottle();
- }
-
- return subject;
+ this.requestNext.add(channelId);
+ this.updateThrottle();
}
updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME);
@@ -109,171 +91,64 @@ class ChannelMetadataRelayLoader {
};
if (query["#e"] && query["#e"].length > 0) this.log(`Updating query`, query["#e"].length);
- this.subscription.setFilters([query]);
+ this.subscription.filters = [query];
- if (this.subscription.state !== NostrSubscription.OPEN) {
- this.subscription.open();
- }
- } else if (this.subscription.state === NostrSubscription.OPEN) {
+ this.subscription.update();
+ } else {
this.subscription.close();
}
}
}
}
-const READ_CACHE_BATCH_TIME = 250;
-const WRITE_CACHE_BATCH_TIME = 250;
-
/** This is a clone of ReplaceableEventLoaderService to support channel metadata */
class ChannelMetadataService {
- private metadata = new SuperMap>(() => new Subject());
-
- private loaders = new SuperMap(
- (relay) => new ChannelMetadataRelayLoader(relay, this.log.extend(relay)),
+ private loaders = new SuperMap(
+ (relay) => new ChannelMetadataRelayLoader(relay, this.log.extend(relay.url)),
);
log = logger.extend("ChannelMetadata");
- dbLog = this.log.extend("database");
- handleEvent(event: NostrEvent, saveToCache = true) {
+ constructor() {
+ if (localRelay) {
+ const loader = this.loaders.get(localRelay as AbstractRelay);
+ loader.isCache = true;
+ }
+ }
+
+ handleEvent(event: NostrEvent) {
eventStore.add(event);
const channelId = getChannelPointer(event)?.id;
if (!channelId) return;
- const sub = this.metadata.get(channelId);
- const current = sub.value;
- if (!current || event.created_at > current.created_at) {
- sub.next(event);
- if (saveToCache) this.saveToCache(channelId, event);
- }
- }
-
- getSubject(channelId: string) {
- return this.metadata.get(channelId);
- }
-
- private readFromCachePromises = new Map>();
- private readFromCacheThrottle = _throttle(this.readFromCache, READ_CACHE_BATCH_TIME);
- private async readFromCache() {
- if (this.readFromCachePromises.size === 0) return;
-
- let read = 0;
- const transaction = db.transaction("channelMetadata", "readonly");
- for (const [channelId, promise] of this.readFromCachePromises) {
- transaction
- .objectStore("channelMetadata")
- .get(channelId)
- .then((cached) => {
- if (cached?.event) {
- this.handleEvent(cached.event, false);
- promise.resolve(true);
- read++;
- }
- promise.resolve(false);
- });
- }
- this.readFromCachePromises.clear();
- transaction.commit();
- await transaction.done;
- if (read > 0) this.dbLog(`Read ${read} events from database`);
- }
- private loadCacheDedupe = new Map>();
- loadFromCache(channelId: string) {
- const dedupe = this.loadCacheDedupe.get(channelId);
- if (dedupe) return dedupe;
-
- // add to read queue
- const promise = createDefer();
- this.readFromCachePromises.set(channelId, promise);
-
- this.loadCacheDedupe.set(channelId, promise);
- this.readFromCacheThrottle();
-
- return promise;
- }
-
- private writeCacheQueue = new Map();
- private writeToCacheThrottle = _throttle(this.writeToCache, WRITE_CACHE_BATCH_TIME);
- private async writeToCache() {
- if (this.writeCacheQueue.size === 0) return;
-
- this.dbLog(`Writing ${this.writeCacheQueue.size} events to database`);
- const transaction = db.transaction("channelMetadata", "readwrite");
- for (const [channelId, event] of this.writeCacheQueue) {
- transaction.objectStore("channelMetadata").put({ channelId, event, created: dayjs().unix() });
- }
- this.writeCacheQueue.clear();
- transaction.commit();
- await transaction.done;
- }
- private async saveToCache(channelId: string, event: NostrEvent) {
- this.writeCacheQueue.set(channelId, event);
- this.writeToCacheThrottle();
- }
-
- async pruneDatabaseCache() {
- const keys = await db.getAllKeysFromIndex(
- "channelMetadata",
- // @ts-ignore
- "created",
- IDBKeyRange.upperBound(dayjs().subtract(1, "week").unix()),
- );
-
- if (keys.length === 0) return;
- this.dbLog(`Pruning ${keys.length} expired events from database`);
- const transaction = db.transaction("channelMetadata", "readwrite");
- for (const key of keys) {
- transaction.store.delete(key);
- }
- await transaction.commit();
+ if (!isFromCache(event)) localRelay?.publish(event);
}
private requestChannelMetadataFromRelays(relays: Iterable, channelId: string) {
- const sub = this.metadata.get(channelId);
-
const relayUrls = Array.from(relays);
- for (const relay of relayUrls) {
- const request = this.loaders.get(relay).requestMetadata(channelId);
-
- sub.connectWithMapper(request, (event, next, current) => {
- if (!current || event.created_at > current.created_at) {
- next(event);
- this.saveToCache(channelId, event);
- }
- });
+ for (const url of relayUrls) {
+ const relay = relayPoolService.getRelay(url);
+ if (relay) this.loaders.get(relay).requestMetadata(channelId);
}
-
- return sub;
}
+ private loaded = new Map();
requestMetadata(relays: Iterable, channelId: string, opts: RequestOptions = {}) {
- const sub = this.metadata.get(channelId);
+ const loaded = this.loaded.get(channelId);
- if (!sub.value) {
- this.loadFromCache(channelId).then((loaded) => {
- if (!loaded && !sub.value) this.requestChannelMetadataFromRelays(relays, channelId);
- });
+ if (!loaded && localRelay) {
+ this.loaders.get(localRelay as AbstractRelay).requestMetadata(channelId);
}
- if (opts?.alwaysRequest || (!sub.value && opts.ignoreCache)) {
+ if (opts?.alwaysRequest || (!loaded && opts.ignoreCache)) {
this.requestChannelMetadataFromRelays(relays, channelId);
}
-
- return sub;
}
}
const channelMetadataService = new ChannelMetadataService();
-channelMetadataService.pruneDatabaseCache();
-setInterval(
- () => {
- channelMetadataService.pruneDatabaseCache();
- },
- 1000 * 60 * 60,
-);
-
if (import.meta.env.DEV) {
//@ts-ignore
window.channelMetadataService = channelMetadataService;
diff --git a/src/services/db/index.ts b/src/services/db/index.ts
index 60eb0f5f7..533d2b4b6 100644
--- a/src/services/db/index.ts
+++ b/src/services/db/index.ts
@@ -1,15 +1,15 @@
import { openDB, deleteDB, IDBPDatabase, IDBPTransaction } from "idb";
import { clearDB, deleteDB as nostrIDBDelete } from "nostr-idb";
-import { SchemaV1, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7, SchemaV8, SchemaV9 } from "./schema";
+import { SchemaV1, SchemaV10, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7, SchemaV9 } from "./schema";
import { logger } from "../../helpers/debug";
import { localDatabase } from "../local-relay";
const log = logger.extend("Database");
const dbName = "storage";
-const version = 9;
-const db = await openDB(dbName, version, {
+const version = 10;
+const db = await openDB(dbName, version, {
upgrade(db, oldVersion, newVersion, transaction, event) {
if (oldVersion < 1) {
const v0 = db as unknown as IDBPDatabase;
@@ -178,6 +178,11 @@ const db = await openDB(dbName, version, {
const readStore = v9.createObjectStore("read", { keyPath: "key" });
readStore.createIndex("ttl", "ttl");
}
+
+ if (oldVersion < 10) {
+ const v9 = db as unknown as IDBPDatabase;
+ v9.deleteObjectStore("channelMetadata");
+ }
},
});
@@ -187,9 +192,6 @@ export async function clearCacheData() {
log("Clearing nostr-idb");
await clearDB(localDatabase);
- log("Clearing channelMetadata");
- await db.clear("channelMetadata");
-
log("Clearing userSearch");
await db.clear("userSearch");
diff --git a/src/services/db/schema.ts b/src/services/db/schema.ts
index 066a51ae4..100a03193 100644
--- a/src/services/db/schema.ts
+++ b/src/services/db/schema.ts
@@ -153,3 +153,5 @@ export interface SchemaV9 extends SchemaV8 {
indexes: { ttl: number };
};
}
+
+export interface SchemaV10 extends Omit {}
diff --git a/src/services/decryption-cache.ts b/src/services/decryption-cache.ts
index b1f3a1388..aa6dc8fcb 100644
--- a/src/services/decryption-cache.ts
+++ b/src/services/decryption-cache.ts
@@ -1,5 +1,5 @@
-import Subject from "../classes/subject";
import _throttle from "lodash.throttle";
+import { BehaviorSubject } from "rxjs";
import createDefer, { Deferred } from "../classes/deferred";
import signingService from "./signing";
@@ -15,8 +15,8 @@ class DecryptionContainer {
pubkey: string;
cipherText: string;
- plaintext = new Subject();
- error = new Subject();
+ plaintext = new BehaviorSubject(undefined);
+ error = new BehaviorSubject(undefined);
constructor(id: string, type: EncryptionType = "nip04", pubkey: string, cipherText: string) {
this.id = id;
diff --git a/src/services/dictionary.ts b/src/services/dictionary.ts
index 93f07c7cc..8e508aa26 100644
--- a/src/services/dictionary.ts
+++ b/src/services/dictionary.ts
@@ -1,12 +1,12 @@
import { NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { EventStore } from "applesauce-core";
+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 Subject from "../classes/subject";
import BatchIdentifierLoader from "../classes/batch-identifier-loader";
import BookOpen01 from "../components/icons/book-open-01";
import processManager from "./process-manager";
@@ -19,7 +19,9 @@ class DictionaryService {
process: Process;
store: EventStore;
- topics = new SuperMap>>(() => new Subject