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>()); + topics = new SuperMap>>( + () => new BehaviorSubject>(new Map()), + ); loaders = new SuperMap((relay) => { const loader = new BatchIdentifierLoader(this.store, relay, [WIKI_PAGE_KIND], this.log.extend(relay.url)); diff --git a/src/services/dns-identity.ts b/src/services/dns-identity.ts index ca7eb3d2c..f1653d363 100644 --- a/src/services/dns-identity.ts +++ b/src/services/dns-identity.ts @@ -1,10 +1,10 @@ import dayjs from "dayjs"; import db from "./db"; import _throttle from "lodash.throttle"; +import { BehaviorSubject } from "rxjs"; import { fetchWithProxy } from "../helpers/request"; import SuperMap from "../classes/super-map"; -import Subject from "../classes/subject"; export function parseAddress(address: string): { name?: string; domain?: string } { const parts = address.trim().toLowerCase().split("@"); @@ -43,7 +43,9 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson): class DnsIdentityService { // undefined === loading - identities = new SuperMap>(() => new Subject()); + identities = new SuperMap>( + () => new BehaviorSubject(undefined), + ); async fetchIdentity(address: string): Promise { const { name, domain } = parseAddress(address); diff --git a/src/services/event-count.ts b/src/services/event-count.ts index b4a890d3d..f5e5df35d 100644 --- a/src/services/event-count.ts +++ b/src/services/event-count.ts @@ -1,12 +1,14 @@ import { Filter } from "nostr-tools"; import stringify from "json-stringify-deterministic"; +import { BehaviorSubject } from "rxjs"; -import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; import { localRelay } from "./local-relay"; class EventCountService { - subjects = new SuperMap>(() => new Subject()); + subjects = new SuperMap>( + () => new BehaviorSubject(undefined), + ); stringifyFilter(filter: Filter | Filter[]) { return stringify(filter); diff --git a/src/services/event-reactions.ts b/src/services/event-reactions.ts index 925ccf566..4ff4ea045 100644 --- a/src/services/event-reactions.ts +++ b/src/services/event-reactions.ts @@ -2,9 +2,7 @@ import { kinds } from "nostr-tools"; import _throttle from "lodash.throttle"; import { AbstractRelay } from "nostr-tools/abstract-relay"; -import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; -import { NostrEvent } from "../types/nostr-event"; import { localRelay } from "./local-relay"; import relayPoolService from "./relay-pool"; import Process from "../classes/process"; @@ -12,20 +10,15 @@ import { LightningIcon } from "../components/icons"; import processManager from "./process-manager"; import BatchRelationLoader from "../classes/batch-relation-loader"; import { logger } from "../helpers/debug"; -import { eventStore } from "./event-store"; class EventReactionsService { log = logger.extend("EventReactionsService"); process: Process; - subjects = new SuperMap>(() => new Subject([])); - + private loaded = new Map(); loaders = new SuperMap((relay) => { const loader = new BatchRelationLoader(relay, [kinds.Reaction], this.log.extend(relay.url)); this.process.addChild(loader.process); - loader.onEventUpdate.subscribe((id) => { - this.updateSubject(id); - }); return loader; }); @@ -37,48 +30,17 @@ class EventReactionsService { processManager.registerProcess(this.process); } - // merged results from all loaders for a single event - private updateSubject(id: string) { - const ids = new Set(); - const events: NostrEvent[] = []; - const subject = this.subjects.get(id); - - for (const [relay, loader] of this.loaders) { - if (loader.references.has(id)) { - const other = loader.references.get(id); - for (const [_, e] of other) { - if (!ids.has(e.id)) { - ids.add(e.id); - events.push(e); - } - } - } - } - - subject.next(events); - } - - requestReactions(eventUID: string, urls: Iterable, alwaysRequest = true) { - const subject = this.subjects.get(eventUID); - if (subject.value && !alwaysRequest) return subject; + requestReactions(uid: string, urls: Iterable, alwaysRequest = false) { + if (this.loaded.get(uid) && !alwaysRequest) return; if (localRelay) { - this.loaders.get(localRelay as AbstractRelay).requestEvents(eventUID); + this.loaders.get(localRelay as AbstractRelay).requestEvents(uid); } const relays = relayPoolService.getRelays(urls); for (const relay of relays) { - this.loaders.get(relay).requestEvents(eventUID); + this.loaders.get(relay).requestEvents(uid); } - - return subject; - } - - handleEvent(event: NostrEvent) { - eventStore.add(event); - - // pretend it came from the local relay - if (localRelay) this.loaders.get(localRelay as AbstractRelay).handleEvent(event); } } diff --git a/src/services/event-zaps.ts b/src/services/event-zaps.ts index 98cd53f1a..707296ccf 100644 --- a/src/services/event-zaps.ts +++ b/src/services/event-zaps.ts @@ -2,9 +2,7 @@ import { kinds } from "nostr-tools"; import _throttle from "lodash.throttle"; import { AbstractRelay } from "nostr-tools/abstract-relay"; -import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; -import { NostrEvent } from "../types/nostr-event"; import { localRelay } from "./local-relay"; import relayPoolService from "./relay-pool"; import Process from "../classes/process"; @@ -17,14 +15,10 @@ class EventZapsService { log = logger.extend("EventZapsService"); process: Process; - subjects = new SuperMap>(() => new Subject([])); - + private loaded = new Map(); loaders = new SuperMap((relay) => { const loader = new BatchRelationLoader(relay, [kinds.Zap], this.log.extend(relay.url)); this.process.addChild(loader.process); - loader.onEventUpdate.subscribe((id) => { - this.updateSubject(id); - }); return loader; }); @@ -36,41 +30,17 @@ class EventZapsService { processManager.registerProcess(this.process); } - // merged results from all loaders for a single event - private updateSubject(id: string) { - const ids = new Set(); - const events: NostrEvent[] = []; - const subject = this.subjects.get(id); - - for (const [relay, loader] of this.loaders) { - if (loader.references.has(id)) { - const other = loader.references.get(id); - for (const [_, e] of other) { - if (!ids.has(e.id)) { - ids.add(e.id); - events.push(e); - } - } - } - } - - subject.next(events); - } - - requestZaps(eventUID: string, urls: Iterable, alwaysRequest = true) { - const subject = this.subjects.get(eventUID); - if (subject.value && !alwaysRequest) return subject; + requestZaps(uid: string, urls: Iterable, alwaysRequest = true) { + if (this.loaded.get(uid) && !alwaysRequest) return; if (localRelay) { - this.loaders.get(localRelay as AbstractRelay).requestEvents(eventUID); + this.loaders.get(localRelay as AbstractRelay).requestEvents(uid); } const relays = relayPoolService.getRelays(urls); for (const relay of relays) { - this.loaders.get(relay).requestEvents(eventUID); + this.loaders.get(relay).requestEvents(uid); } - - return subject; } } diff --git a/src/services/read-status.ts b/src/services/read-status.ts index 0611e3438..1f283b7ff 100644 --- a/src/services/read-status.ts +++ b/src/services/read-status.ts @@ -1,14 +1,16 @@ import dayjs from "dayjs"; import _throttle from "lodash.throttle"; +import { BehaviorSubject } from "rxjs"; -import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; import db from "./db"; import { logger } from "../helpers/debug"; class ReadStatusService { log = logger.extend("ReadStatusService"); - status = new SuperMap>(() => new Subject()); + status = new SuperMap>( + () => new BehaviorSubject(undefined), + ); ttl = new Map(); private setTTL(key: string, ttl: number) { diff --git a/src/services/relay-stats.ts b/src/services/relay-stats.ts index 5987cf70b..f9c7aeb16 100644 --- a/src/services/relay-stats.ts +++ b/src/services/relay-stats.ts @@ -1,21 +1,26 @@ import _throttle from "lodash.throttle"; +import { Filter } from "nostr-tools"; +import { BehaviorSubject } from "rxjs"; -import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; import relayInfoService from "./relay-info"; import { localRelay } from "./local-relay"; import { MONITOR_STATS_KIND, SELF_REPORTED_KIND, getRelayURL } from "../helpers/nostr/relay-stats"; import relayPoolService from "./relay-pool"; -import { Filter } from "nostr-tools"; import { alwaysVerify } from "./verify-event"; +import { eventStore } from "./event-store"; const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f"; -const MONITOR_RELAY = "wss://history.nostr.watch"; +const MONITOR_RELAY = "wss://relay.nostr.watch"; class RelayStatsService { - private selfReported = new SuperMap>(() => new Subject()); - private monitorStats = new SuperMap>(() => new Subject()); + private selfReported = new SuperMap>( + () => new BehaviorSubject(undefined), + ); + private monitorStats = new SuperMap>( + () => new BehaviorSubject(undefined), + ); constructor() { // load all stats from cache and subscribe to future ones @@ -33,6 +38,8 @@ class RelayStatsService { const relay = getRelayURL(event); if (!relay) return; + eventStore.add(event); + const sub = this.monitorStats.get(relay); if (event.kind === SELF_REPORTED_KIND) { if (!sub.value || event.created_at > sub.value.created_at) { diff --git a/src/views/badges/badge-details.tsx b/src/views/badges/badge-details.tsx index d6ef21d73..00c4a3b6e 100644 --- a/src/views/badges/badge-details.tsx +++ b/src/views/badges/badge-details.tsx @@ -25,7 +25,6 @@ import useTimelineLoader from "../../hooks/use-timeline-loader"; import { useReadRelays } from "../../hooks/use-client-relays"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; -import useSubject from "../../hooks/use-subject"; import { NostrEvent } from "../../types/nostr-event"; import { getEventCoordinate } from "../../helpers/nostr/event"; import UserAvatarLink from "../../components/user/user-avatar-link"; diff --git a/src/views/community/views/newest.tsx b/src/views/community/views/newest.tsx index 87ac08a4e..e606b4ebc 100644 --- a/src/views/community/views/newest.tsx +++ b/src/views/community/views/newest.tsx @@ -1,7 +1,6 @@ import { useOutletContext } from "react-router-dom"; import { COMMUNITY_APPROVAL_KIND, buildApprovalMap, getCommunityMods } from "../../../helpers/nostr/communities"; -import useSubject from "../../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status"; diff --git a/src/views/community/views/trending.tsx b/src/views/community/views/trending.tsx deleted file mode 100644 index db2c58ba3..000000000 --- a/src/views/community/views/trending.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useMemo } from "react"; -import { useOutletContext } from "react-router-dom"; -import { useObservable } from "applesauce-react/hooks"; - -import { - COMMUNITY_APPROVAL_KIND, - buildApprovalMap, - getCommunityMods, - getCommunityRelays, -} from "../../../helpers/nostr/communities"; -import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; -import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; -import TimelineActionAndStatus from "../../../components/timeline/timeline-action-and-status"; -import useUserMuteFilter from "../../../hooks/use-user-mute-filter"; -import useEventsReactions from "../../../hooks/use-events-reactions"; -import { getEventReactionScore, groupReactions } from "../../../helpers/nostr/reactions"; -import ApprovedEvent from "../components/community-approved-post"; -import { RouterContext } from "../community-home"; - -export default function CommunityTrendingView() { - const { community, timeline } = useOutletContext(); - const muteFilter = useUserMuteFilter(); - const mods = getCommunityMods(community); - - const events = useObservable(timeline.timeline) ?? []; - const approvalMap = buildApprovalMap(events, mods); - - const approved = events - .filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && approvalMap.has(e.id)) - .map((event) => ({ event, approvals: approvalMap.get(event.id) })) - .filter((e) => !muteFilter(e.event)); - - // fetch votes for approved posts - const eventReactions = useEventsReactions( - approved.map((e) => e.event.id), - getCommunityRelays(community), - ); - const eventVotes = useMemo(() => { - const dir: Record = {}; - for (const [id, reactions] of Object.entries(eventReactions)) { - const grouped = groupReactions(reactions); - const { vote } = getEventReactionScore(grouped); - dir[id] = vote; - } - return dir; - }, [eventReactions]); - - const sorted = approved.sort((a, b) => (eventVotes[b.event.id] ?? 0) - (eventVotes[a.event.id] ?? 0)); - - const callback = useTimelineCurserIntersectionCallback(timeline); - - return ( - <> - - {sorted.map(({ event, approvals }) => ( - - ))} - - - - ); -} diff --git a/src/views/dms/components/direct-message-content.tsx b/src/views/dms/components/direct-message-content.tsx index 0fc92e3e1..96793ef60 100644 --- a/src/views/dms/components/direct-message-content.tsx +++ b/src/views/dms/components/direct-message-content.tsx @@ -27,7 +27,6 @@ import { components } from "../../../components/content"; import { useKind4Decrypt } from "../../../hooks/use-kind4-decryption"; import { fedimintTokens } from "../../../helpers/fedimint"; -const transformers = [...defaultTransformers, fedimintTokens]; const linkRenderers = [ renderSimpleXLink, renderYoutubeURL, @@ -54,7 +53,7 @@ export default function DirectMessageContent({ ...props }: { event: NostrEvent; text: string } & BoxProps) { const { plaintext } = useKind4Decrypt(event); - const content = useRenderedContent(plaintext, components, { transformers, linkRenderers }); + const content = useRenderedContent(plaintext, components, { linkRenderers }); return ( diff --git a/src/views/goals/components/goal-progress.tsx b/src/views/goals/components/goal-progress.tsx index d12977920..b1573632a 100644 --- a/src/views/goals/components/goal-progress.tsx +++ b/src/views/goals/components/goal-progress.tsx @@ -1,4 +1,5 @@ import { Flex, Progress, Text } from "@chakra-ui/react"; + import { NostrEvent } from "../../../types/nostr-event"; import { getGoalAmount, getGoalRelays } from "../../../helpers/nostr/goal"; import { LightningIcon } from "../../../components/icons"; diff --git a/src/views/goals/components/goal-top-zappers.tsx b/src/views/goals/components/goal-top-zappers.tsx index a5d5060d6..6d79e1669 100644 --- a/src/views/goals/components/goal-top-zappers.tsx +++ b/src/views/goals/components/goal-top-zappers.tsx @@ -1,6 +1,6 @@ -import { Box, Flex, FlexProps, Text } from "@chakra-ui/react"; +import { Box, Flex, FlexProps } from "@chakra-ui/react"; +import { getEventUID, getZapPayment, getZapSender } from "applesauce-core/helpers"; -import { getEventUID } from "../../../helpers/nostr/event"; import { getGoalRelays } from "../../../helpers/nostr/goal"; import useEventZaps from "../../../hooks/use-event-zaps"; import { NostrEvent } from "../../../types/nostr-event"; @@ -16,15 +16,13 @@ export default function GoalTopZappers({ }: Omit & { goal: NostrEvent; max?: number }) { const zaps = useEventZaps(getEventUID(goal), getGoalRelays(goal), true); - const totals: Record = {}; - for (const zap of zaps) { - const p = zap.request.pubkey; - if (zap.payment.amount) { - totals[p] = (totals[p] || 0) + zap.payment.amount; - } - } + const totals = zaps?.reduce>((dir, z) => { + const sender = getZapSender(z); + dir[sender] = dir[sender] + (getZapPayment(z)?.amount ?? 0); + return dir; + }, {}); - const sortedTotals = Array.from(Object.entries(totals)).sort((a, b) => b[1] - a[1]); + const sortedTotals = totals ? Array.from(Object.entries(totals)).sort((a, b) => b[1] - a[1]) : []; if (max !== undefined) { sortedTotals.length = max; } diff --git a/src/views/goals/components/goal-zap-list.tsx b/src/views/goals/components/goal-zap-list.tsx index 1ab44de46..6afb27dfa 100644 --- a/src/views/goals/components/goal-zap-list.tsx +++ b/src/views/goals/components/goal-zap-list.tsx @@ -1,37 +1,47 @@ import { Box, Flex, Spacer, Text } from "@chakra-ui/react"; -import { getEventUID } from "../../../helpers/nostr/event"; +import { getEventUID, getZapPayment, getZapRequest, getZapSender } from "applesauce-core/helpers"; +import { NostrEvent } from "nostr-tools"; + import { getGoalRelays } from "../../../helpers/nostr/goal"; import useEventZaps from "../../../hooks/use-event-zaps"; -import { NostrEvent } from "../../../types/nostr-event"; import UserAvatarLink from "../../../components/user/user-avatar-link"; import UserLink from "../../../components/user/user-link"; import { readablizeSats } from "../../../helpers/bolt11"; import { LightningIcon } from "../../../components/icons"; import Timestamp from "../../../components/timestamp"; +import TextNoteContents from "../../../components/note/timeline-note/text-note-contents"; + +function GoalZap({ zap }: { zap: NostrEvent }) { + const request = getZapRequest(zap); + const payment = getZapPayment(zap); + const sender = getZapSender(zap); + if (!payment?.amount) return null; + + return ( + + + + + + + + {request.content && } + + + + {readablizeSats(payment.amount / 1000)} + + + ); +} export default function GoalZapList({ goal }: { goal: NostrEvent }) { const zaps = useEventZaps(getEventUID(goal), getGoalRelays(goal), true); - const sorted = Array.from(zaps).sort((a, b) => b.event.created_at - a.event.created_at); return ( <> - {sorted.map((zap) => ( - - - - - - - - {zap.request.content && {zap.request.content}} - - - {zap.payment.amount && ( - - {readablizeSats(zap.payment.amount / 1000)} - - )} - + {zaps.map((zap) => ( + ))} ); diff --git a/src/views/launchpad/components/notifications-card.tsx b/src/views/launchpad/components/notifications-card.tsx index a6b678171..39497c0ce 100644 --- a/src/views/launchpad/components/notifications-card.tsx +++ b/src/views/launchpad/components/notifications-card.tsx @@ -1,6 +1,7 @@ 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"; @@ -9,7 +10,6 @@ import { useNotifications } from "../../../providers/global/notifications-provid import { NotificationType, NotificationTypeSymbol } from "../../../classes/notifications"; import NotificationItem from "../../notifications/components/notification-item"; import { ErrorBoundary } from "../../../components/error-boundary"; -import { useObservable } from "../../../hooks/use-observable"; export default function NotificationsCard({ ...props }: Omit) { const navigate = useNavigate(); diff --git a/src/views/notifications/components/notification-item.tsx b/src/views/notifications/components/notification-item.tsx index c74b58d5e..1ed211b1c 100644 --- a/src/views/notifications/components/notification-item.tsx +++ b/src/views/notifications/components/notification-item.tsx @@ -64,7 +64,7 @@ const NotificationItem = ({ content = ; break; case NotificationType.Zap: - content = ; + content = ; break; case NotificationType.Message: content = ; diff --git a/src/views/notifications/components/zap-notificaiton.tsx b/src/views/notifications/components/zap-notificaiton.tsx index 286571f69..954df8b6c 100644 --- a/src/views/notifications/components/zap-notificaiton.tsx +++ b/src/views/notifications/components/zap-notificaiton.tsx @@ -1,10 +1,15 @@ -import { ReactNode, forwardRef, useMemo } from "react"; +import { ReactNode, forwardRef } from "react"; import { AvatarGroup, ButtonGroup, Flex, Text } from "@chakra-ui/react"; +import { + getZapAddressPointer, + getZapEventPointer, + getZapPayment, + getZapRequest, + getZapSender, +} from "applesauce-core/helpers"; +import { NostrEvent } from "nostr-tools"; -import { NostrEvent, isATag, isETag } from "../../../types/nostr-event"; -import { getParsedZap } from "../../../helpers/nostr/zaps"; import { readablizeSats } from "../../../helpers/bolt11"; -import { parseCoordinate } from "../../../helpers/nostr/event"; import { EmbedEventPointer } from "../../../components/embed-event"; import UserAvatarLink from "../../../components/user/user-avatar-link"; import { LightningIcon } from "../../../components/icons"; @@ -12,58 +17,54 @@ import NotificationIconEntry from "./notification-icon-entry"; import ZapReceiptMenu from "../../../components/zap/zap-receipt-menu"; import TextNoteContents from "../../../components/note/timeline-note/text-note-contents"; -const ZapNotification = forwardRef void }>( - ({ event, onClick }, ref) => { - const zap = useMemo(() => getParsedZap(event), [event]); +const ZapNotification = forwardRef void }>( + ({ zap, onClick }, ref) => { + const payment = getZapPayment(zap); + const request = getZapRequest(zap); + if (!payment?.amount) return null; - if (!zap || !zap.payment.amount) return null; - - const eventId = zap?.request.tags.find(isETag)?.[1]; - const coordinate = zap?.request.tags.find(isATag)?.[1]; - const parsedCoordinate = coordinate ? parseCoordinate(coordinate) : null; + const sender = getZapSender(zap); + const nevent = getZapEventPointer(zap); + const naddr = getZapAddressPointer(zap); let eventJSX: ReactNode | null = null; - if (parsedCoordinate && parsedCoordinate.identifier) { + if (naddr) { eventJSX = ( ); - } else if (eventId) { - eventJSX = ; + } else if (nevent) { + eventJSX = ; } return ( } - id={event.id} - pubkey={zap.request.pubkey} - timestamp={zap.request.created_at} + id={zap.id} + pubkey={sender} + timestamp={request.created_at} summary={ <> - {readablizeSats(zap.payment.amount / 1000)} {zap.request.content} + {readablizeSats(payment.amount / 1000)} {request.content} } onClick={onClick} > - + - zapped {readablizeSats(zap.payment.amount / 1000)} sats + zapped {readablizeSats(payment.amount / 1000)} sats - + - + {eventJSX} ); diff --git a/src/views/notifications/index.tsx b/src/views/notifications/index.tsx index 977801cef..4afa785b5 100644 --- a/src/views/notifications/index.tsx +++ b/src/views/notifications/index.tsx @@ -3,10 +3,11 @@ import { Button, ButtonGroup, Divider, Flex, Text } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import dayjs, { Dayjs } from "dayjs"; import { getEventUID } from "nostr-idb"; +import { BehaviorSubject } from "rxjs"; +import { useObservable } from "applesauce-react/hooks"; import RequireCurrentAccount from "../../providers/route/require-current-account"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; -import useSubject from "../../hooks/use-subject"; 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"; @@ -24,8 +25,6 @@ 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 { PersistentSubject } from "../../classes/subject"; -import { useObservable } from "../../hooks/use-observable"; function TimeMarker({ date, ids }: { date: Dayjs; ids: string[] }) { const readAll = useCallback(() => { @@ -151,14 +150,14 @@ const NotificationsTimeline = memo( }, ); -const cachedFocus = new PersistentSubject(""); +const cachedFocus = new BehaviorSubject(""); function NotificationsPage() { const { timeline } = useNotifications(); // const { value: focused, setValue: setFocused } = useRouteStateValue("focused", ""); // const [focused, setFocused] = useState(""); - const focused = useSubject(cachedFocus); + const focused = useObservable(cachedFocus); const setFocused = useCallback((id: string) => cachedFocus.next(id), [cachedFocus]); const focusContext = useMemo(() => ({ id: focused, focus: setFocused }), [focused, setFocused]); diff --git a/src/views/notifications/threads.tsx b/src/views/notifications/threads.tsx index 4e61fc55a..49dbe32ea 100644 --- a/src/views/notifications/threads.tsx +++ b/src/views/notifications/threads.tsx @@ -7,7 +7,6 @@ import useCurrentAccount from "../../hooks/use-current-account"; import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider"; import RequireCurrentAccount from "../../providers/route/require-current-account"; import VerticalPageLayout from "../../components/vertical-page-layout"; -import useSubject from "../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { useNotifications } from "../../providers/global/notifications-provider"; import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents"; @@ -31,7 +30,7 @@ import GitBranch01 from "../../components/icons/git-branch-01"; const THREAD_KINDS = [kinds.ShortTextNote, TORRENT_COMMENT_KIND]; function ReplyEntry({ event }: { event: NostrEvent }) { - const enableDrawer = useSubject(localSettings.enableNoteThreadDrawer); + const enableDrawer = useObservable(localSettings.enableNoteThreadDrawer); const navigate = enableDrawer ? useNavigateInDrawer() : useNavigate(); const address = useShareableEventAddress(event); const onClick = useCallback( diff --git a/src/views/relays/cache/database/internal.tsx b/src/views/relays/cache/database/internal.tsx index 4f835d864..c09427c4e 100644 --- a/src/views/relays/cache/database/internal.tsx +++ b/src/views/relays/cache/database/internal.tsx @@ -18,6 +18,7 @@ import { } from "@chakra-ui/react"; import { useAsync } from "react-use"; import { NostrEvent } from "nostr-tools"; +import { useObservable } from "applesauce-react/hooks"; import { localDatabase } from "../../../../services/local-relay"; import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-chart"; @@ -25,7 +26,6 @@ import EventKindsTable from "../../../../components/charts/event-kinds-table"; import ImportEventsButton from "./components/import-events-button"; import ExportEventsButton from "./components/export-events-button"; import { clearCacheData, deleteDatabase } from "../../../../services/db"; -import useSubject from "../../../../hooks/use-subject"; import localSettings from "../../../../services/local-settings"; async function importEvents(events: NostrEvent[]) { @@ -43,7 +43,7 @@ export default function InternalDatabasePage() { const { value: count } = useAsync(async () => await countEvents(localDatabase), []); const { value: kinds } = useAsync(async () => await countEventsByKind(localDatabase), []); - const maxEvents = useSubject(localSettings.idbMaxEvents); + const maxEvents = useObservable(localSettings.idbMaxEvents); const [clearing, setClearing] = useState(false); const handleClearData = async () => { diff --git a/src/views/relays/cache/database/wasm.tsx b/src/views/relays/cache/database/wasm.tsx index ea58656a7..6a489303d 100644 --- a/src/views/relays/cache/database/wasm.tsx +++ b/src/views/relays/cache/database/wasm.tsx @@ -15,6 +15,7 @@ import { Text, } from "@chakra-ui/react"; import { NostrEvent } from "nostr-tools"; +import { useObservable } from "applesauce-react/hooks"; import { localRelay } from "../../../../services/local-relay"; import WasmRelay from "../../../../services/wasm-relay"; @@ -22,7 +23,6 @@ import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-ch import EventKindsTable from "../../../../components/charts/event-kinds-table"; import ImportEventsButton from "./components/import-events-button"; import ExportEventsButton from "./components/export-events-button"; -import useSubject from "../../../../hooks/use-subject"; import localSettings from "../../../../services/local-settings"; export default function WasmDatabasePage() { @@ -32,7 +32,7 @@ export default function WasmDatabasePage() { if (!worker) return null; const [summary, setSummary] = useState>(); - const persistForDays = useSubject(localSettings.wasmPersistForDays); + const persistForDays = useObservable(localSettings.wasmPersistForDays); const total = summary ? Object.values(summary).reduce((t, v) => t + v, 0) : undefined; diff --git a/src/views/relays/webrtc/connect.tsx b/src/views/relays/webrtc/connect.tsx index acd26a1a7..33d44d8b1 100644 --- a/src/views/relays/webrtc/connect.tsx +++ b/src/views/relays/webrtc/connect.tsx @@ -12,6 +12,7 @@ import { useInterval, } from "@chakra-ui/react"; 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"; @@ -20,7 +21,6 @@ import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-but import UserAvatar from "../../../components/user/user-avatar"; import UserName from "../../../components/user/user-name"; import localSettings from "../../../services/local-settings"; -import useSubject from "../../../hooks/use-subject"; export default function WebRtcConnectView() { const update = useForceUpdate(); @@ -46,7 +46,7 @@ export default function WebRtcConnectView() { reset(); }); - const recent = useSubject(localSettings.webRtcRecentConnections) + const recent = useObservable(localSettings.webRtcRecentConnections) .map((uri) => ({ ...NostrWebRtcBroker.parseNostrWebRtcURI(uri), uri })) .filter(({ pubkey }) => !webRtcRelaysService.broker.peers.has(pubkey)); diff --git a/src/views/relays/webrtc/pair.tsx b/src/views/relays/webrtc/pair.tsx index f7c4f58af..11e67fa88 100644 --- a/src/views/relays/webrtc/pair.tsx +++ b/src/views/relays/webrtc/pair.tsx @@ -17,22 +17,22 @@ import { useInterval, } from "@chakra-ui/react"; import { getPublicKey, kinds, nip19 } from "nostr-tools"; +import { useForm } from "react-hook-form"; +import dayjs from "dayjs"; +import { useAsync } from "react-use"; +import { useObservable } from "applesauce-react/hooks"; import BackButton from "../../../components/router/back-button"; import webRtcRelaysService from "../../../services/webrtc-relays"; -import useSubject from "../../../hooks/use-subject"; import localSettings from "../../../services/local-settings"; import { CopyIconButton } from "../../../components/copy-icon-button"; import UserAvatar from "../../../components/user/user-avatar"; import UserName from "../../../components/user/user-name"; import QrCodeSvg from "../../../components/qr-code/qr-code-svg"; import { QrCodeIcon } from "../../../components/icons"; -import { useForm } from "react-hook-form"; -import dayjs from "dayjs"; import { usePublishEvent } from "../../../providers/global/publish-provider"; import useCurrentAccount from "../../../hooks/use-current-account"; import useUserProfile from "../../../hooks/use-user-profile"; -import { useAsync } from "react-use"; function NameForm() { const publish = usePublishEvent(); @@ -85,7 +85,7 @@ export default function WebRtcPairView() { const account = useCurrentAccount(); const showQrCode = useDisclosure(); - const identity = useSubject(localSettings.webRtcLocalIdentity); + const identity = useObservable(localSettings.webRtcLocalIdentity); const pubkey = useMemo(() => getPublicKey(identity), [identity]); const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]); diff --git a/src/views/settings/display/index.tsx b/src/views/settings/display/index.tsx index 985dbd406..cb404ac55 100644 --- a/src/views/settings/display/index.tsx +++ b/src/views/settings/display/index.tsx @@ -12,8 +12,8 @@ import { Heading, Button, } from "@chakra-ui/react"; +import { useObservable } from "applesauce-react/hooks"; -import useSubject from "../../../hooks/use-subject"; import localSettings from "../../../services/local-settings"; import useSettingsForm from "../use-settings-form"; import VerticalPageLayout from "../../../components/vertical-page-layout"; @@ -21,9 +21,9 @@ import VerticalPageLayout from "../../../components/vertical-page-layout"; export default function DisplaySettings() { const { register, submit, formState } = useSettingsForm(); - const hideZapBubbles = useSubject(localSettings.hideZapBubbles); - const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer); - const showBrandLogo = useSubject(localSettings.showBrandLogo); + const hideZapBubbles = useObservable(localSettings.hideZapBubbles); + const enableNoteDrawer = useObservable(localSettings.enableNoteThreadDrawer); + const showBrandLogo = useObservable(localSettings.showBrandLogo); return ( diff --git a/src/views/settings/performance/index.tsx b/src/views/settings/performance/index.tsx index 8f0c54079..55868835d 100644 --- a/src/views/settings/performance/index.tsx +++ b/src/views/settings/performance/index.tsx @@ -11,15 +11,15 @@ import { Button, Heading, } from "@chakra-ui/react"; +import { useObservable } from "applesauce-react/hooks"; import { safeUrl } from "../../../helpers/parse"; import VerticalPageLayout from "../../../components/vertical-page-layout"; import useSettingsForm from "../use-settings-form"; -import useSubject from "../../../hooks/use-subject"; import localSettings from "../../../services/local-settings"; function VerifyEventSettings() { - const verifyEventMethod = useSubject(localSettings.verifyEventMethod); + const verifyEventMethod = useObservable(localSettings.verifyEventMethod); return ( <> @@ -46,7 +46,7 @@ function VerifyEventSettings() { export default function PerformanceSettings() { const { register, submit, formState } = useSettingsForm(); - const enableKeyboardShortcuts = useSubject(localSettings.enableKeyboardShortcuts); + const enableKeyboardShortcuts = useObservable(localSettings.enableKeyboardShortcuts); return ( diff --git a/src/views/settings/post/index.tsx b/src/views/settings/post/index.tsx index 81b174da0..33ecdf2ef 100644 --- a/src/views/settings/post/index.tsx +++ b/src/views/settings/post/index.tsx @@ -22,6 +22,7 @@ import { Switch, } from "@chakra-ui/react"; import { matchSorter } from "match-sorter"; +import { useObservable } from "applesauce-react/hooks"; import { EditIcon } from "../../../components/icons"; import { useContextEmojis } from "../../../providers/global/emoji-provider"; @@ -30,7 +31,6 @@ import useCurrentAccount from "../../../hooks/use-current-account"; import useSettingsForm from "../use-settings-form"; import VerticalPageLayout from "../../../components/vertical-page-layout"; import localSettings from "../../../services/local-settings"; -import useSubject from "../../../hooks/use-subject"; export default function PostSettings() { const account = useCurrentAccount(); @@ -67,7 +67,7 @@ export default function PostSettings() { ); }; - const addClientTag = useSubject(localSettings.addClientTag); + const addClientTag = useObservable(localSettings.addClientTag); return ( diff --git a/src/views/settings/privacy/index.tsx b/src/views/settings/privacy/index.tsx index b74e912e6..32e87e948 100644 --- a/src/views/settings/privacy/index.tsx +++ b/src/views/settings/privacy/index.tsx @@ -12,12 +12,13 @@ import { Heading, FormLabel, } from "@chakra-ui/react"; +import { useObservable } from "applesauce-react/hooks"; + import { safeUrl } from "../../../helpers/parse"; import { createRequestProxyUrl } from "../../../helpers/request"; import { RelayAuthMode } from "../../../classes/relay-pool"; import VerticalPageLayout from "../../../components/vertical-page-layout"; import useSettingsForm from "../use-settings-form"; -import useSubject from "../../../hooks/use-subject"; import localSettings from "../../../services/local-settings"; async function validateInvidiousUrl(url?: string) { @@ -45,8 +46,8 @@ async function validateRequestProxy(url?: string) { export default function PrivacySettings() { const { register, submit, formState } = useSettingsForm(); - const defaultAuthenticationMode = useSubject(localSettings.defaultAuthenticationMode); - const proactivelyAuthenticate = useSubject(localSettings.proactivelyAuthenticate); + const defaultAuthenticationMode = useObservable(localSettings.defaultAuthenticationMode); + const proactivelyAuthenticate = useObservable(localSettings.proactivelyAuthenticate); return ( diff --git a/src/views/streams/components/top-zappers.tsx b/src/views/streams/components/top-zappers.tsx index 49a0d55a2..85313a67f 100644 --- a/src/views/streams/components/top-zappers.tsx +++ b/src/views/streams/components/top-zappers.tsx @@ -1,8 +1,7 @@ -import { useMemo } from "react"; import { Flex, FlexProps, Text } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; +import { getZapPayment, getZapSender } from "applesauce-core/helpers"; -import { parseZapEvents } from "../../../helpers/nostr/zaps"; import UserLink from "../../../components/user/user-link"; import { LightningIcon } from "../../../components/icons"; import { readablizeSats } from "../../../helpers/bolt11"; @@ -12,15 +11,13 @@ import UserAvatarLink from "../../../components/user/user-avatar-link"; export default function TopZappers({ stream, ...props }: FlexProps & { stream: ParsedStream }) { const { timeline } = useStreamChatTimeline(stream); - const zaps = useMemo(() => parseZapEvents(timeline.filter((e) => e.kind === kinds.Zap)), [timeline]); + const zaps = timeline.filter((e) => e.kind === kinds.Zap); - const totals: Record = {}; - for (const zap of zaps) { - const p = zap.request.pubkey; - if (zap.payment.amount) { - totals[p] = (totals[p] || 0) + zap.payment.amount; - } - } + const totals = zaps?.reduce>((dir, z) => { + const sender = getZapSender(z); + dir[sender] = dir[sender] + (getZapPayment(z)?.amount ?? 0); + return dir; + }, {}); const sortedTotals = Array.from(Object.entries(totals)).sort((a, b) => b[1] - a[1]); diff --git a/src/views/streams/stream/index.tsx b/src/views/streams/stream/index.tsx index fcf9647d9..a2c02064e 100644 --- a/src/views/streams/stream/index.tsx +++ b/src/views/streams/stream/index.tsx @@ -200,7 +200,7 @@ function StreamPage({ stream }: { stream: ParsedStream }) { const Layout = isMobile ? MobileStreamPage : DesktopStreamPage; // const chatTimeline = useStreamChatTimeline(stream); - // const chatLog = useSubject(chatTimeline.timeline); + // const chatLog = useObservable(chatTimeline.timeline); // const pubkeysInChat = useMemo(() => { // const set = new Set(); // for (const event of chatLog) { diff --git a/src/views/streams/stream/stream-chat/zap-message.tsx b/src/views/streams/stream/stream-chat/zap-message.tsx index f90d9c5a7..9a31c4c2e 100644 --- a/src/views/streams/stream/stream-chat/zap-message.tsx +++ b/src/views/streams/stream/stream-chat/zap-message.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from "react"; +import { memo } from "react"; import { Box, Flex, Text } from "@chakra-ui/react"; import { ParsedStream } from "../../../../helpers/nostr/stream"; @@ -6,33 +6,35 @@ import UserAvatar from "../../../../components/user/user-avatar"; import UserLink from "../../../../components/user/user-link"; import { NostrEvent } from "../../../../types/nostr-event"; import { LightningIcon } from "../../../../components/icons"; -import { getParsedZap } from "../../../../helpers/nostr/zaps"; import { readablizeSats } from "../../../../helpers/bolt11"; import { TrustProvider } from "../../../../providers/local/trust-provider"; import ChatMessageContent from "./chat-message-content"; import useClientSideMuteFilter from "../../../../hooks/use-client-side-mute-filter"; import useEventIntersectionRef from "../../../../hooks/use-event-intersection-ref"; +import { getZapPayment, getZapRequest, getZapSender } from "applesauce-core/helpers"; function ZapMessage({ zap, stream }: { zap: NostrEvent; stream: ParsedStream }) { const ref = useEventIntersectionRef(zap); - const parsed = useMemo(() => getParsedZap(zap), [zap]); + const sender = getZapSender(zap); + const payment = getZapPayment(zap); + const request = getZapRequest(zap); const clientMuteFilter = useClientSideMuteFilter(); - if (!parsed || !parsed.payment.amount) return null; - if (clientMuteFilter(parsed.event)) return null; + if (!payment?.amount) return null; + if (clientMuteFilter(zap)) return null; return ( - + - - - zapped {readablizeSats(parsed.payment.amount / 1000)} sats + + + zapped {readablizeSats(payment.amount / 1000)} sats - + diff --git a/src/views/task-manager/modal.tsx b/src/views/task-manager/modal.tsx index 347d7b17a..5f4ca2910 100644 --- a/src/views/task-manager/modal.tsx +++ b/src/views/task-manager/modal.tsx @@ -1,3 +1,4 @@ +import { Suspense } from "react"; import { Heading, Modal, @@ -7,21 +8,9 @@ import { ModalOverlay, ModalProps, Spinner, - Tab, - TabIndicator, - TabList, - TabPanel, - TabPanels, - Tabs, } from "@chakra-ui/react"; import { RouterProvider, createMemoryRouter } from "react-router-dom"; -import { PersistentSubject } from "../../classes/subject"; -import useSubject from "../../hooks/use-subject"; -import DatabaseView from "../relays/cache/database"; -import TaskManagerRelays from "./relays"; -import { Suspense } from "react"; - type Router = ReturnType; export default function TaskManagerModal({ diff --git a/src/views/task-manager/publish-log/action-status-tag.tsx b/src/views/task-manager/publish-log/action-status-tag.tsx index 5b13900b6..997d7da1f 100644 --- a/src/views/task-manager/publish-log/action-status-tag.tsx +++ b/src/views/task-manager/publish-log/action-status-tag.tsx @@ -1,14 +1,14 @@ import { Spinner, Tag, TagLabel, TagProps } from "@chakra-ui/react"; +import { useObservable } from "applesauce-react/hooks"; import PublishAction from "../../../classes/nostr-publish-action"; -import useSubject from "../../../hooks/use-subject"; import { CheckIcon, ErrorIcon } from "../../../components/icons"; export default function PublishActionStatusTag({ action, ...props }: { action: PublishAction } & Omit) { - const results = useSubject(action.results); + const results = useObservable(action.results); const successful = results.filter(({ success }) => success); const failedWithMessage = results.filter(({ success, message }) => !success && !!message); diff --git a/src/views/task-manager/publish-log/publish-details.tsx b/src/views/task-manager/publish-log/publish-details.tsx index 826faded4..36113330e 100644 --- a/src/views/task-manager/publish-log/publish-details.tsx +++ b/src/views/task-manager/publish-log/publish-details.tsx @@ -11,9 +11,9 @@ import { Spinner, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; import PublishAction, { PublishResult } from "../../../classes/nostr-publish-action"; -import useSubject from "../../../hooks/use-subject"; import { RelayPaidTag } from "../../relays/components/relay-card"; import { EmbedEvent } from "../../../components/embed-event"; @@ -39,7 +39,7 @@ function PublishResultRow({ result }: { result: PublishResult }) { } export function PublishDetails({ pub }: PostResultsProps & Omit) { - const results = useSubject(pub.results); + const results = useObservable(pub.results); const relayResults: Record = {}; for (const url of pub.relays) { diff --git a/src/views/task-manager/relays/index.tsx b/src/views/task-manager/relays/index.tsx index bf6ec37b8..b4e31e5db 100644 --- a/src/views/task-manager/relays/index.tsx +++ b/src/views/task-manager/relays/index.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { Badge, Box, @@ -20,12 +21,13 @@ import { import { Link as RouterLink } from "react-router-dom"; import { useLocalStorage } from "react-use"; import { AbstractRelay } from "nostr-tools/abstract-relay"; +import { useObservable } from "applesauce-react/hooks"; +import { combineLatest, map } from "rxjs"; import relayPoolService from "../../../services/relay-pool"; import { RelayFavicon } from "../../../components/relay-favicon"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; import { localRelay } from "../../../services/local-relay"; -import useSubjects from "../../../hooks/use-subjects"; import { IconRelayAuthButton, useRelayAuthMethod } from "../../../components/relays/relay-auth-button"; import RelayConnectSwitch from "../../../components/relays/relay-connect-switch"; import useRouteSearchValue from "../../../hooks/use-route-search-value"; @@ -33,7 +35,6 @@ import processManager from "../../../services/process-manager"; import { RelayAuthMode } from "../../../classes/relay-pool"; import Timestamp from "../../../components/timestamp"; import localSettings from "../../../services/local-settings"; -import useSubject from "../../../hooks/use-subject"; function RelayCard({ relay }: { relay: AbstractRelay }) { return ( @@ -52,7 +53,7 @@ function RelayCard({ relay }: { relay: AbstractRelay }) { function RelayAuthCard({ relay }: { relay: AbstractRelay }) { const { authenticated } = useRelayAuthMethod(relay); - const defaultMode = useSubject(localSettings.defaultAuthenticationMode); + const defaultMode = useObservable(localSettings.defaultAuthenticationMode); const processes = processManager.getRootProcessesForRelay(relay); const [authMode, setAuthMode] = useLocalStorage( @@ -106,9 +107,14 @@ export default function TaskManagerRelays() { .filter((r) => r !== localRelay) .sort((a, b) => +b.connected - +a.connected || a.url.localeCompare(b.url)); - const notices = useSubjects(Array.from(relayPoolService.notices.values())) - .flat() - .sort((a, b) => b.date - a.date); + const observable = useMemo( + () => + combineLatest(Array.from(relayPoolService.notices.values())).pipe( + map((relays) => relays.flat().sort((a, b) => b.date - a.date)), + ), + [], + ); + const notices = useObservable(observable) ?? []; const challenges = Array.from(relayPoolService.challenges.entries()).filter(([r, c]) => r.connected && !!c.value); diff --git a/src/views/task-manager/relays/inspect-relay.tsx b/src/views/task-manager/relays/inspect-relay.tsx index 79860296e..bfaa8e639 100644 --- a/src/views/task-manager/relays/inspect-relay.tsx +++ b/src/views/task-manager/relays/inspect-relay.tsx @@ -14,11 +14,11 @@ import { useInterval, } from "@chakra-ui/react"; import { useParams } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; import VerticalPageLayout from "../../../components/vertical-page-layout"; import BackButton from "../../../components/router/back-button"; import relayPoolService from "../../../services/relay-pool"; -import useSubject from "../../../hooks/use-subject"; import ProcessBranch from "../processes/process/process-tree"; import processManager from "../../../services/process-manager"; @@ -37,7 +37,7 @@ export default function InspectRelayView() { const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]); const rootProcesses = processManager.getRootProcessesForRelay(relay); - const notices = useSubject(relayPoolService.notices.get(relay)); + const notices = useObservable(relayPoolService.notices.get(relay)); return ( diff --git a/src/views/thread/components/tabs/zaps.tsx b/src/views/thread/components/tabs/zaps.tsx index eeed7b90b..dcdf041d2 100644 --- a/src/views/thread/components/tabs/zaps.tsx +++ b/src/views/thread/components/tabs/zaps.tsx @@ -1,8 +1,9 @@ import { memo } from "react"; import { Box, ButtonGroup, Flex, Text } from "@chakra-ui/react"; import { ThreadItem } from "applesauce-core/queries"; +import { NostrEvent } from "nostr-tools"; +import { getZapPayment, getZapRequest, getZapSender } from "applesauce-core/helpers"; -import { ParsedZap } from "../../../../helpers/nostr/zaps"; import UserAvatarLink from "../../../../components/user/user-avatar-link"; import UserLink from "../../../../components/user/user-link"; import Timestamp from "../../../../components/timestamp"; @@ -12,39 +13,42 @@ import TextNoteContents from "../../../../components/note/timeline-note/text-not import { TrustProvider } from "../../../../providers/local/trust-provider"; import ZapReceiptMenu from "../../../../components/zap/zap-receipt-menu"; -const ZapEvent = memo(({ zap }: { zap: ParsedZap }) => { - if (!zap.payment.amount) return null; +const ZapEvent = memo(({ zap }: { zap: NostrEvent }) => { + const request = getZapRequest(zap); + const payment = getZapPayment(zap); + const sender = getZapSender(zap); + if (!payment?.amount) return null; return ( - + - {readablizeSats(zap.payment.amount / 1000)} + {readablizeSats(payment.amount / 1000)} - + - - - + + + - + ); }); -export default function PostZapsTab({ post, zaps }: { post: ThreadItem; zaps: ParsedZap[] }) { +export default function PostZapsTab({ post, zaps }: { post: ThreadItem; zaps: NostrEvent[] }) { return ( {Array.from(zaps) - .sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0)) + .sort((a, b) => (getZapPayment(b)?.amount ?? 0) - (getZapPayment(a)?.amount ?? 0)) .map((zap) => ( - + ))} ); diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx index 3d9e73fd8..65f0c1c9c 100644 --- a/src/views/user/zaps.tsx +++ b/src/views/user/zaps.tsx @@ -2,6 +2,7 @@ import { ReactNode, useCallback, useMemo, useState } from "react"; import { useOutletContext } from "react-router-dom"; import { Box, Flex, Select, Text } from "@chakra-ui/react"; import { useRenderedContent } from "applesauce-react/hooks"; +import { getZapPayment, getZapRequest } from "applesauce-core/helpers"; import dayjs from "dayjs"; import { ErrorBoundary } from "../../components/error-boundary"; @@ -9,7 +10,7 @@ import { LightningIcon } from "../../components/icons"; import UserAvatarLink from "../../components/user/user-avatar-link"; import UserLink from "../../components/user/user-link"; import { readablizeSats } from "../../helpers/bolt11"; -import { isProfileZap, isNoteZap, totalZaps, parseZapEvents, getParsedZap } from "../../helpers/nostr/zaps"; +import { isProfileZap, isNoteZap, totalZaps } from "../../helpers/nostr/zaps"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { NostrEvent, isATag, isETag } from "../../types/nostr-event"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; @@ -27,10 +28,11 @@ import { renderGenericUrl } from "../../components/content/links/common"; const linkRenderers = [renderGenericUrl]; -const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => { - const ref = useEventIntersectionRef(zapEvent); +const Zap = ({ zap }: { zap: NostrEvent }) => { + const ref = useEventIntersectionRef(zap); - const { request, payment } = getParsedZap(zapEvent, false); + const request = getZapRequest(zap); + const payment = getZapPayment(zap); const eventId = request.tags.find(isETag)?.[1]; const coordinate = request.tags.find(isATag)?.[1]; @@ -62,7 +64,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => { Zapped - {payment.amount && ( + {payment?.amount && ( {readablizeSats(payment.amount / 1000)} sats @@ -95,13 +97,12 @@ const UserZapsTab = () => { [filter], ); - const { loader, timeline: events } = useTimelineLoader( + const { loader, timeline: zaps } = useTimelineLoader( `${pubkey}-zaps`, relays, { "#p": [pubkey], kinds: [9735] }, { eventFilter }, ); - const zaps = useMemo(() => parseZapEvents(events), [events]); const callback = useTimelineCurserIntersectionCallback(loader); @@ -114,19 +115,19 @@ const UserZapsTab = () => { - {events.length && ( + {zaps.length && ( {readablizeSats(totalZaps(zaps) / 1000)} sats in the last{" "} - {dayjs.unix(events[events.length - 1].created_at).fromNow(true)} + {dayjs.unix(zaps[zaps.length - 1].created_at).fromNow(true)} )} - {events.map((event) => ( - - + {zaps.map((zaps) => ( + + ))} diff --git a/src/views/wiki/components/wiki-link.tsx b/src/views/wiki/components/wiki-link.tsx index bce7b8213..e9fab67b8 100644 --- a/src/views/wiki/components/wiki-link.tsx +++ b/src/views/wiki/components/wiki-link.tsx @@ -18,12 +18,12 @@ import { NostrEvent } from "nostr-tools"; import { ExtraProps } from "react-markdown"; import { getEventUID } from "nostr-idb"; import { Link as RouterLink } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { getPageDefer, getPageSummary } from "../../../helpers/nostr/wiki"; import UserName from "../../../components/user/user-name"; import dictionaryService from "../../../services/dictionary"; -import useSubject from "../../../hooks/use-subject"; import { useWebOfTrust } from "../../../providers/global/web-of-trust-provider"; export default function WikiLink({ @@ -47,7 +47,7 @@ export default function WikiLink({ () => (topic ? dictionaryService.requestTopic(topic, readRelays) : undefined), [topic, readRelays], ); - const events = useSubject(subject); + const events = useObservable(subject); const sorted = useMemo(() => { if (!events) return []; diff --git a/src/views/wiki/page.tsx b/src/views/wiki/page.tsx index 20f825e2e..008ee7054 100644 --- a/src/views/wiki/page.tsx +++ b/src/views/wiki/page.tsx @@ -14,6 +14,7 @@ import { Text, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; @@ -21,7 +22,6 @@ import VerticalPageLayout from "../../components/vertical-page-layout"; import { getPageDefer, getPageForks, getPageSummary, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki"; import MarkdownContent from "./components/markdown"; import UserLink from "../../components/user/user-link"; -import useSubject from "../../hooks/use-subject"; import WikiPageResult from "./components/wiki-page-result"; import Timestamp from "../../components/timestamp"; import WikiPageHeader from "./components/wiki-page-header"; @@ -142,7 +142,7 @@ function WikiPageFooter({ page }: { page: NostrEvent }) { const readRelays = useReadRelays(); const subject = useMemo(() => dictionaryService.requestTopic(topic, readRelays), [topic, readRelays]); - const pages = useSubject(subject); + const pages = useObservable(subject); let forks = pages ? Array.from(pages.values()).filter((p) => getPageForks(p).address?.pubkey === page.pubkey) : []; if (webOfTrust) forks = webOfTrust.sortByDistanceAndConnections(forks, (p) => p.pubkey); diff --git a/src/views/wiki/topic.tsx b/src/views/wiki/topic.tsx index f2cc9689c..f7cf5bd4a 100644 --- a/src/views/wiki/topic.tsx +++ b/src/views/wiki/topic.tsx @@ -1,9 +1,9 @@ import { Button, Flex, Heading, Link } from "@chakra-ui/react"; import { Navigate, useParams, Link as RouterLink } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; import { NostrEvent } from "nostr-tools"; import VerticalPageLayout from "../../components/vertical-page-layout"; -import useSubject from "../../hooks/use-subject"; import { useMemo } from "react"; import dictionaryService from "../../services/dictionary"; import { useReadRelays } from "../../hooks/use-client-relays"; @@ -23,7 +23,7 @@ export default function WikiTopicView() { const readRelays = useReadRelays(); const subject = useMemo(() => dictionaryService.requestTopic(topic, readRelays, true), [topic, readRelays]); - const pages = useSubject(subject); + const pages = useObservable(subject); let sorted = pages ? Array.from(pages.values()) : []; if (webOfTrust) sorted = webOfTrust.sortByDistanceAndConnections(sorted, (p) => p.pubkey);