mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
use applesauce core for zaps
This commit is contained in:
parent
48e939e572
commit
5ea8604997
5
.changeset/purple-poets-develop.md
Normal file
5
.changeset/purple-poets-develop.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Cleanup zap parsing
|
5
.changeset/warm-dancers-peel.md
Normal file
5
.changeset/warm-dancers-peel.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Remove old community trending view
|
72
pnpm-lock.yaml
generated
72
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
@ -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: <CommunityView />,
|
||||
children: [
|
||||
{ path: "", element: <CommunityNewestView /> },
|
||||
{ path: "trending", element: <CommunityTrendingView /> },
|
||||
{ path: "newest", element: <CommunityNewestView /> },
|
||||
{ path: "pending", element: <CommunityPendingView /> },
|
||||
],
|
||||
|
@ -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 {
|
||||
|
@ -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 */
|
||||
|
@ -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";
|
||||
|
@ -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++;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PersistentSubject } from "../subject";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
export class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
|
||||
export class NullableLocalStorageEntry<T = string> extends BehaviorSubject<T | null> {
|
||||
key: string;
|
||||
decode?: (raw: string | null) => T | null;
|
||||
encode?: (value: T) => string | null;
|
||||
@ -44,7 +44,7 @@ export class NullableLocalStorageEntry<T = string> extends PersistentSubject<T |
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalStorageEntry<T = string> extends PersistentSubject<T> {
|
||||
export class LocalStorageEntry<T = string> extends BehaviorSubject<T> {
|
||||
key: string;
|
||||
fallback: T;
|
||||
decode?: (raw: string) => T;
|
||||
|
@ -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<PublishResult[]>([]);
|
||||
results = new BehaviorSubject<PublishResult[]>([]);
|
||||
completePromise = createDefer();
|
||||
|
||||
/** @deprecated */
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<AbstractRelay>();
|
||||
onRelayChallenge = new Subject<[AbstractRelay, string]>();
|
||||
|
||||
notices = new SuperMap<AbstractRelay, PersistentSubject<Notice[]>>(() => new PersistentSubject<Notice[]>([]));
|
||||
notices = new SuperMap<AbstractRelay, BehaviorSubject<Notice[]>>(() => new BehaviorSubject<Notice[]>([]));
|
||||
|
||||
connectionErrors = new SuperMap<AbstractRelay, Error[]>(() => []);
|
||||
connecting = new SuperMap<AbstractRelay, PersistentSubject<boolean>>(() => new PersistentSubject(false));
|
||||
connecting = new SuperMap<AbstractRelay, BehaviorSubject<boolean>>(() => new BehaviorSubject(false));
|
||||
|
||||
challenges = new SuperMap<AbstractRelay, Subject<string>>(() => new Subject<string>());
|
||||
authForPublish = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
|
||||
authForSubscribe = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
|
||||
challenges = new SuperMap<AbstractRelay, BehaviorSubject<string | undefined>>(
|
||||
() => new BehaviorSubject<string | undefined>(undefined),
|
||||
);
|
||||
authForPublish = new SuperMap<AbstractRelay, BehaviorSubject<boolean | undefined>>(
|
||||
() => new BehaviorSubject<boolean | undefined>(undefined),
|
||||
);
|
||||
authForSubscribe = new SuperMap<AbstractRelay, BehaviorSubject<boolean | undefined>>(
|
||||
() => new BehaviorSubject<boolean | undefined>(undefined),
|
||||
);
|
||||
|
||||
authenticated = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
|
||||
authenticated = new SuperMap<AbstractRelay, BehaviorSubject<boolean>>(() => new BehaviorSubject(false));
|
||||
|
||||
getRelay(relayOrUrl: string | URL | AbstractRelay) {
|
||||
if (typeof relayOrUrl === "string") {
|
||||
|
@ -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<T> {
|
||||
private observable: ControlledObservable<T>;
|
||||
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<T>["subscribe"];
|
||||
|
||||
once(next: (value: T) => void) {
|
||||
const sub = this.subscribe((v) => {
|
||||
if (v !== undefined) {
|
||||
next(v);
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
return sub;
|
||||
}
|
||||
|
||||
map<R>(callback: (value: T) => R, defaultValue?: R): Subject<R> {
|
||||
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<R>(
|
||||
subject: Subject<R>,
|
||||
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<T> extends Subject<T> {
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
<JsonCode data={getThreadReferences(event)} />
|
||||
</Section>
|
||||
<Section label="Tags">
|
||||
<DebugEventTags event={event} />
|
||||
<ErrorBoundary>
|
||||
<DebugEventTags event={event} />
|
||||
</ErrorBoundary>
|
||||
<Heading size="sm">Tags referenced in content</Heading>
|
||||
<JsonCode data={getContentTagRefs(event.content, event.tags)} />
|
||||
</Section>
|
||||
|
@ -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<CardProps, "children"> & { 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)}`;
|
||||
|
||||
|
@ -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<CardProps, "children"> & { torrent: NostrEvent }) {
|
||||
const enableDrawer = useSubject(localSettings.enableNoteThreadDrawer);
|
||||
const enableDrawer = useObservable(localSettings.enableNoteThreadDrawer);
|
||||
const navigate = enableDrawer ? useNavigateInDrawer() : useNavigate();
|
||||
const link = `/torrents/${getSharableEventAddress(torrent)}`;
|
||||
|
||||
|
@ -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<CardProps, "children"> & { 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 (
|
||||
<Card as={LinkBox} {...props}>
|
||||
<CardHeader display="flex" p="2" gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={parsed.request.pubkey} size="sm" />
|
||||
<UserLink pubkey={parsed.request.pubkey} fontWeight="bold" />
|
||||
<UserAvatar pubkey={sender} size="sm" />
|
||||
<UserLink pubkey={sender} fontWeight="bold" />
|
||||
<Text>Zapped</Text>
|
||||
<UserLink pubkey={recipient} fontWeight="bold" />
|
||||
|
||||
{parsed.payment.amount && (
|
||||
{payment.amount && (
|
||||
<>
|
||||
<LightningIcon color="yellow.500" boxSize={5} />
|
||||
<Text>{readablizeSats(parsed.payment.amount / 1000)}</Text>
|
||||
<Text>{readablizeSats(payment.amount / 1000)}</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Timestamp timestamp={parsed.event.created_at} ml="auto" />
|
||||
<Timestamp timestamp={zap.created_at} ml="auto" />
|
||||
<ButtonGroup size="sm" variant="ghost">
|
||||
<ZapReceiptMenu zap={zap} aria-label="More Options" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
<CardBody px="2" pb="2" pt="0" display="flex" flexDirection="column" gap="2">
|
||||
<Box>
|
||||
<TextNoteContents event={parsed.request} />
|
||||
<TextNoteContents event={request} />
|
||||
</Box>
|
||||
|
||||
{pointer && <EmbedEventPointer pointer={pointer} />}
|
||||
|
@ -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<CodeProps, "children">) {
|
||||
const enableKeyboardShortcuts = useSubject(localSettings.enableKeyboardShortcuts);
|
||||
const enableKeyboardShortcuts = useObservable(localSettings.enableKeyboardShortcuts);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useKeyPressEvent(
|
||||
(e) => (requireMeta ? e.ctrlKey || e.metaKey : true) && e.key === letter,
|
||||
|
@ -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";
|
||||
|
@ -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<FlexProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
const offline = useObservable(offlineMode);
|
||||
const showBrandLogo = useSubject(localSettings.showBrandLogo);
|
||||
const showBrandLogo = useObservable(localSettings.showBrandLogo);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
|
@ -11,6 +11,7 @@ import ZapModal from "../event-zap-modal";
|
||||
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
||||
import { getEventUID } from "../../helpers/nostr/event";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import { getZapSender } from "applesauce-core/helpers";
|
||||
|
||||
export type NoteZapButtonProps = Omit<ButtonProps, "children"> & {
|
||||
event: NostrEvent;
|
||||
@ -21,10 +22,10 @@ export type NoteZapButtonProps = Omit<ButtonProps, "children"> & {
|
||||
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 = () => {
|
||||
|
@ -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 (
|
||||
<Tag key={zap.id} borderRadius="full" py="1" flexShrink={0} variant="outline">
|
||||
<LightningIcon mr="1" color="yellow.400" />
|
||||
<TagLabel fontWeight="bold">{readablizeSats((payment.amount ?? 0) / 1000)}</TagLabel>
|
||||
<UserAvatar pubkey={request.pubkey} size="xs" square={false} ml="2" />
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
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<FlexProps, "children">) {
|
||||
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 (
|
||||
<HiddenScrollbar overflowY="hidden" overflowX="auto" gap="2" {...props}>
|
||||
{sorted.map((zap) => (
|
||||
<Tag key={zap.event.id} borderRadius="full" py="1" flexShrink={0} variant="outline">
|
||||
<LightningIcon mr="1" color="yellow.400" />
|
||||
<TagLabel fontWeight="bold">{readablizeSats((zap.payment.amount ?? 0) / 1000)}</TagLabel>
|
||||
<UserAvatar pubkey={zap.request.pubkey} size="xs" square={false} ml="2" />
|
||||
</Tag>
|
||||
<ZapBubble key={zap.id} zap={zap} />
|
||||
))}
|
||||
</HiddenScrollbar>
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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<PublishAction>();
|
||||
|
@ -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 () => {
|
||||
|
@ -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<HTMLInputElement> = async (e) => {
|
||||
try {
|
||||
|
@ -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 <Badge colorScheme={getStatusColor(relay!, connecting)}>{getStatusText(relay!, connecting)}</Badge>;
|
||||
};
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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<null | string> {
|
||||
export async function getZapEndpoint(metadata: ProfileContent): Promise<null | string> {
|
||||
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 }[];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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<string>, alwaysRequest = true) {
|
||||
export default function useEventZaps(uid: string, additionalRelays?: Iterable<string>, 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) ?? [];
|
||||
}
|
||||
|
@ -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<string>,
|
||||
alwaysRequest = true,
|
||||
) {
|
||||
const readRelays = useReadRelays(additionalRelays);
|
||||
|
||||
// get subjects
|
||||
const subjects = useMemo(() => {
|
||||
const dir: Record<string, Subject<NostrEvent[]>> = {};
|
||||
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<string, NostrEvent[]> = {};
|
||||
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;
|
||||
}
|
@ -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);
|
||||
|
@ -1,3 +0,0 @@
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
|
||||
export { useObservable };
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Subject, { PersistentSubject } from "../classes/subject";
|
||||
|
||||
function useSubject<Value extends unknown>(subject: PersistentSubject<Value>): Value;
|
||||
function useSubject<Value extends unknown>(subject?: PersistentSubject<Value>): Value | undefined;
|
||||
function useSubject<Value extends unknown>(subject?: Subject<Value>): Value | undefined;
|
||||
function useSubject<Value extends unknown>(subject?: Subject<Value>) {
|
||||
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;
|
@ -1,21 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Subject, { PersistentSubject } from "../classes/subject";
|
||||
|
||||
function useSubjects<Value extends unknown>(
|
||||
subjects: (Subject<Value> | PersistentSubject<Value> | 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;
|
@ -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 };
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<IntersectionObserverEntry[]>([]));
|
||||
const [subject] = useState(() => new BehaviorSubject<IntersectionObserverEntry[]>([]));
|
||||
|
||||
const handleIntersection = useCallback<IntersectionObserverCallback>(
|
||||
(entries, observer) => {
|
||||
|
@ -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<string, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||
private subscription: PersistentSubscription;
|
||||
|
||||
private requestNext = new Set<string>();
|
||||
private requested = new Map<string, Date>();
|
||||
|
||||
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<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||
|
||||
private loaders = new SuperMap<Relay, ChannelMetadataRelayLoader>(
|
||||
(relay) => new ChannelMetadataRelayLoader(relay, this.log.extend(relay)),
|
||||
private loaders = new SuperMap<AbstractRelay, ChannelMetadataRelayLoader>(
|
||||
(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<string, Deferred<boolean>>();
|
||||
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<string, Promise<boolean>>();
|
||||
loadFromCache(channelId: string) {
|
||||
const dedupe = this.loadCacheDedupe.get(channelId);
|
||||
if (dedupe) return dedupe;
|
||||
|
||||
// add to read queue
|
||||
const promise = createDefer<boolean>();
|
||||
this.readFromCachePromises.set(channelId, promise);
|
||||
|
||||
this.loadCacheDedupe.set(channelId, promise);
|
||||
this.readFromCacheThrottle();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private writeCacheQueue = new Map<string, NostrEvent>();
|
||||
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<string>, 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<string, boolean>();
|
||||
requestMetadata(relays: Iterable<string>, 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;
|
||||
|
@ -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<SchemaV9>(dbName, version, {
|
||||
const version = 10;
|
||||
const db = await openDB<SchemaV10>(dbName, version, {
|
||||
upgrade(db, oldVersion, newVersion, transaction, event) {
|
||||
if (oldVersion < 1) {
|
||||
const v0 = db as unknown as IDBPDatabase<SchemaV1>;
|
||||
@ -178,6 +178,11 @@ const db = await openDB<SchemaV9>(dbName, version, {
|
||||
const readStore = v9.createObjectStore("read", { keyPath: "key" });
|
||||
readStore.createIndex("ttl", "ttl");
|
||||
}
|
||||
|
||||
if (oldVersion < 10) {
|
||||
const v9 = db as unknown as IDBPDatabase<SchemaV9>;
|
||||
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");
|
||||
|
||||
|
@ -153,3 +153,5 @@ export interface SchemaV9 extends SchemaV8 {
|
||||
indexes: { ttl: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface SchemaV10 extends Omit<SchemaV9, "channelMetadata"> {}
|
||||
|
@ -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<string>();
|
||||
error = new Subject<Error>();
|
||||
plaintext = new BehaviorSubject<string | undefined>(undefined);
|
||||
error = new BehaviorSubject<Error | undefined>(undefined);
|
||||
|
||||
constructor(id: string, type: EncryptionType = "nip04", pubkey: string, cipherText: string) {
|
||||
this.id = id;
|
||||
|
@ -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<string, Subject<Map<string, NostrEvent>>>(() => new Subject<Map<string, NostrEvent>>());
|
||||
topics = new SuperMap<string, BehaviorSubject<Map<string, NostrEvent>>>(
|
||||
() => new BehaviorSubject<Map<string, NostrEvent>>(new Map()),
|
||||
);
|
||||
|
||||
loaders = new SuperMap<AbstractRelay, BatchIdentifierLoader>((relay) => {
|
||||
const loader = new BatchIdentifierLoader(this.store, relay, [WIKI_PAGE_KIND], this.log.extend(relay.url));
|
||||
|
@ -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<string, Subject<DnsIdentity | undefined>>(() => new Subject());
|
||||
identities = new SuperMap<string, BehaviorSubject<DnsIdentity | undefined>>(
|
||||
() => new BehaviorSubject<DnsIdentity | undefined>(undefined),
|
||||
);
|
||||
|
||||
async fetchIdentity(address: string): Promise<DnsIdentity> {
|
||||
const { name, domain } = parseAddress(address);
|
||||
|
@ -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<string, Subject<number>>(() => new Subject<number>());
|
||||
subjects = new SuperMap<string, BehaviorSubject<number | undefined>>(
|
||||
() => new BehaviorSubject<number | undefined>(undefined),
|
||||
);
|
||||
|
||||
stringifyFilter(filter: Filter | Filter[]) {
|
||||
return stringify(filter);
|
||||
|
@ -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<string, Subject<NostrEvent[]>>(() => new Subject<NostrEvent[]>([]));
|
||||
|
||||
private loaded = new Map<string, boolean>();
|
||||
loaders = new SuperMap<AbstractRelay, BatchRelationLoader>((relay) => {
|
||||
const loader = new BatchRelationLoader(relay, [kinds.Reaction], this.log.extend(relay.url));
|
||||
this.process.addChild(loader.process);
|
||||
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<string>();
|
||||
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<string | URL | AbstractRelay>, alwaysRequest = true) {
|
||||
const subject = this.subjects.get(eventUID);
|
||||
if (subject.value && !alwaysRequest) return subject;
|
||||
requestReactions(uid: string, urls: Iterable<string | URL | AbstractRelay>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<string, Subject<NostrEvent[]>>(() => new Subject<NostrEvent[]>([]));
|
||||
|
||||
private loaded = new Map<string, boolean>();
|
||||
loaders = new SuperMap<AbstractRelay, BatchRelationLoader>((relay) => {
|
||||
const loader = new BatchRelationLoader(relay, [kinds.Zap], this.log.extend(relay.url));
|
||||
this.process.addChild(loader.process);
|
||||
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<string>();
|
||||
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<string | URL | AbstractRelay>, alwaysRequest = true) {
|
||||
const subject = this.subjects.get(eventUID);
|
||||
if (subject.value && !alwaysRequest) return subject;
|
||||
requestZaps(uid: string, urls: Iterable<string | URL | AbstractRelay>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<string, Subject<boolean>>(() => new Subject());
|
||||
status = new SuperMap<string, BehaviorSubject<boolean | undefined>>(
|
||||
() => new BehaviorSubject<boolean | undefined>(undefined),
|
||||
);
|
||||
ttl = new Map<string, number>();
|
||||
|
||||
private setTTL(key: string, ttl: number) {
|
||||
|
@ -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<string, Subject<NostrEvent | null>>(() => new Subject());
|
||||
private monitorStats = new SuperMap<string, Subject<NostrEvent>>(() => new Subject());
|
||||
private selfReported = new SuperMap<string, BehaviorSubject<NostrEvent | null | undefined>>(
|
||||
() => new BehaviorSubject<NostrEvent | null | undefined>(undefined),
|
||||
);
|
||||
private monitorStats = new SuperMap<string, BehaviorSubject<NostrEvent | undefined>>(
|
||||
() => new BehaviorSubject<NostrEvent | undefined>(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) {
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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<RouterContext>();
|
||||
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<string, number> = {};
|
||||
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 (
|
||||
<>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{sorted.map(({ event, approvals }) => (
|
||||
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<TrustProvider event={event}>
|
||||
|
@ -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";
|
||||
|
@ -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<FlexProps, "children"> & { goal: NostrEvent; max?: number }) {
|
||||
const zaps = useEventZaps(getEventUID(goal), getGoalRelays(goal), true);
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
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<Record<string, number>>((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;
|
||||
}
|
||||
|
@ -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 (
|
||||
<Flex gap="2">
|
||||
<UserAvatarLink pubkey={sender} size="md" />
|
||||
<Box>
|
||||
<Text>
|
||||
<UserLink fontSize="lg" fontWeight="bold" pubkey={sender} mr="2" />
|
||||
<Timestamp timestamp={zap.created_at} />
|
||||
</Text>
|
||||
{request.content && <TextNoteContents event={request} />}
|
||||
</Box>
|
||||
<Spacer />
|
||||
<Text>
|
||||
<LightningIcon /> {readablizeSats(payment.amount / 1000)}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
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) => (
|
||||
<Flex key={zap.eventId} gap="2">
|
||||
<UserAvatarLink pubkey={zap.request.pubkey} size="md" />
|
||||
<Box>
|
||||
<Text>
|
||||
<UserLink fontSize="lg" fontWeight="bold" pubkey={zap.request.pubkey} mr="2" />
|
||||
<Timestamp timestamp={zap.event.created_at} />
|
||||
</Text>
|
||||
{zap.request.content && <Text>{zap.request.content}</Text>}
|
||||
</Box>
|
||||
<Spacer />
|
||||
{zap.payment.amount && (
|
||||
<Text>
|
||||
<LightningIcon /> {readablizeSats(zap.payment.amount / 1000)}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
{zaps.map((zap) => (
|
||||
<GoalZap key={zap.id} zap={zap} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -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<CardProps, "children">) {
|
||||
const navigate = useNavigate();
|
||||
|
@ -64,7 +64,7 @@ const NotificationItem = ({
|
||||
content = <RepostNotification event={event} onClick={onClick && handleClick} />;
|
||||
break;
|
||||
case NotificationType.Zap:
|
||||
content = <ZapNotification event={event} onClick={onClick && handleClick} />;
|
||||
content = <ZapNotification zap={event} onClick={onClick && handleClick} />;
|
||||
break;
|
||||
case NotificationType.Message:
|
||||
content = <MessageNotification event={event} onClick={onClick && handleClick} />;
|
||||
|
@ -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<HTMLDivElement, { event: NostrEvent; onClick?: () => void }>(
|
||||
({ event, onClick }, ref) => {
|
||||
const zap = useMemo(() => getParsedZap(event), [event]);
|
||||
const ZapNotification = forwardRef<HTMLDivElement, { zap: NostrEvent; onClick?: () => 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 = (
|
||||
<EmbedEventPointer
|
||||
pointer={{
|
||||
type: "naddr",
|
||||
data: {
|
||||
pubkey: parsedCoordinate.pubkey,
|
||||
identifier: parsedCoordinate.identifier,
|
||||
kind: parsedCoordinate.kind,
|
||||
},
|
||||
data: naddr,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (eventId) {
|
||||
eventJSX = <EmbedEventPointer pointer={{ type: "note", data: eventId }} />;
|
||||
} else if (nevent) {
|
||||
eventJSX = <EmbedEventPointer pointer={{ type: "nevent", data: nevent }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationIconEntry
|
||||
ref={ref}
|
||||
icon={<LightningIcon boxSize={6} color="yellow.400" />}
|
||||
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}
|
||||
>
|
||||
<Flex gap="2" alignItems="center" pl="2">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={zap.request.pubkey} />
|
||||
<UserAvatarLink pubkey={sender} />
|
||||
</AvatarGroup>
|
||||
<Text>zapped {readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
||||
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
|
||||
<ButtonGroup size="sm" variant="ghost" ml="auto">
|
||||
<ZapReceiptMenu zap={zap.event} aria-label="More Options" />
|
||||
<ZapReceiptMenu zap={zap} aria-label="More Options" />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<TextNoteContents event={zap.request} />
|
||||
<TextNoteContents event={request} />
|
||||
{eventJSX}
|
||||
</NotificationIconEntry>
|
||||
);
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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<MouseEventHandler>(
|
||||
|
4
src/views/relays/cache/database/internal.tsx
vendored
4
src/views/relays/cache/database/internal.tsx
vendored
@ -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 () => {
|
||||
|
4
src/views/relays/cache/database/wasm.tsx
vendored
4
src/views/relays/cache/database/wasm.tsx
vendored
@ -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<Record<string, number>>();
|
||||
const persistForDays = useSubject(localSettings.wasmPersistForDays);
|
||||
const persistForDays = useObservable(localSettings.wasmPersistForDays);
|
||||
|
||||
const total = summary ? Object.values(summary).reduce((t, v) => t + v, 0) : undefined;
|
||||
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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 (
|
||||
<VerticalPageLayout flex={1}>
|
||||
|
@ -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 (
|
||||
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
|
||||
|
@ -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 (
|
||||
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
|
||||
|
@ -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 (
|
||||
<VerticalPageLayout as="form" onSubmit={submit} flex={1}>
|
||||
|
@ -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<string, number> = {};
|
||||
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<Record<string, number>>((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]);
|
||||
|
||||
|
@ -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<string>();
|
||||
// for (const event of chatLog) {
|
||||
|
@ -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 (
|
||||
<TrustProvider event={parsed.request}>
|
||||
<TrustProvider event={request}>
|
||||
<Flex direction="column" borderRadius="md" borderColor="yellow.400" borderWidth="1px" p="2" ref={ref}>
|
||||
<Flex gap="2">
|
||||
<LightningIcon color="yellow.400" />
|
||||
<UserAvatar pubkey={parsed.request.pubkey} size="xs" />
|
||||
<UserLink pubkey={parsed.request.pubkey} fontWeight="bold" color="yellow.400" />
|
||||
<Text>zapped {readablizeSats(parsed.payment.amount / 1000)} sats</Text>
|
||||
<UserAvatar pubkey={sender} size="xs" />
|
||||
<UserLink pubkey={sender} fontWeight="bold" color="yellow.400" />
|
||||
<Text>zapped {readablizeSats(payment.amount / 1000)} sats</Text>
|
||||
</Flex>
|
||||
<Box>
|
||||
<ChatMessageContent event={parsed.request} />
|
||||
<ChatMessageContent event={request} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
|
@ -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<typeof createMemoryRouter>;
|
||||
|
||||
export default function TaskManagerModal({
|
||||
|
@ -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<TagProps, "children">) {
|
||||
const results = useSubject(action.results);
|
||||
const results = useObservable(action.results);
|
||||
|
||||
const successful = results.filter(({ success }) => success);
|
||||
const failedWithMessage = results.filter(({ success, message }) => !success && !!message);
|
||||
|
@ -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<FlexProps, "children">) {
|
||||
const results = useSubject(pub.results);
|
||||
const results = useObservable(pub.results);
|
||||
|
||||
const relayResults: Record<string, PublishResult | undefined> = {};
|
||||
for (const url of pub.relays) {
|
||||
|
@ -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<RelayAuthMode | "">(
|
||||
@ -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);
|
||||
|
||||
|
@ -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 (
|
||||
<VerticalPageLayout>
|
||||
|
@ -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 (
|
||||
<TrustProvider event={zap.request}>
|
||||
<TrustProvider event={request}>
|
||||
<Flex gap="2">
|
||||
<Flex direction="column" alignItems="center" minW="10">
|
||||
<LightningIcon color="yellow.500" boxSize={5} />
|
||||
<Text>{readablizeSats(zap.payment.amount / 1000)}</Text>
|
||||
<Text>{readablizeSats(payment.amount / 1000)}</Text>
|
||||
</Flex>
|
||||
|
||||
<UserAvatarLink pubkey={zap.request.pubkey} size="sm" ml="2" />
|
||||
<UserAvatarLink pubkey={sender} size="sm" ml="2" />
|
||||
<Box>
|
||||
<UserLink pubkey={zap.request.pubkey} fontWeight="bold" />
|
||||
<Timestamp timestamp={zap.event.created_at} ml="2" />
|
||||
<TextNoteContents event={zap.request} />
|
||||
<UserLink pubkey={sender} fontWeight="bold" />
|
||||
<Timestamp timestamp={zap.created_at} ml="2" />
|
||||
<TextNoteContents event={request} />
|
||||
</Box>
|
||||
|
||||
<ButtonGroup ml="auto" size="sm" variant="ghost">
|
||||
<ZapReceiptMenu zap={zap.event} aria-label="More Options" />
|
||||
<ZapReceiptMenu zap={zap} aria-label="More Options" />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</TrustProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export default function PostZapsTab({ post, zaps }: { post: ThreadItem; zaps: ParsedZap[] }) {
|
||||
export default function PostZapsTab({ post, zaps }: { post: ThreadItem; zaps: NostrEvent[] }) {
|
||||
return (
|
||||
<Flex px="2" direction="column" gap="2" mb="2">
|
||||
{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) => (
|
||||
<ZapEvent key={zap.event.id} zap={zap} />
|
||||
<ZapEvent key={zap.id} zap={zap} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
|
@ -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 }) => {
|
||||
<UserAvatarLink pubkey={request.pubkey} size="sm" />
|
||||
<UserLink pubkey={request.pubkey} fontWeight="bold" />
|
||||
<Text>Zapped</Text>
|
||||
{payment.amount && (
|
||||
{payment?.amount && (
|
||||
<Flex gap="2">
|
||||
<LightningIcon color="yellow.400" />
|
||||
<Text>{readablizeSats(payment.amount / 1000)} sats</Text>
|
||||
@ -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 = () => {
|
||||
<option value="note">Note Zaps</option>
|
||||
<option value="profile">Profile Zaps</option>
|
||||
</Select>
|
||||
{events.length && (
|
||||
{zaps.length && (
|
||||
<Flex gap="2">
|
||||
<LightningIcon color="yellow.400" />
|
||||
<Text>
|
||||
{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)}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
{events.map((event) => (
|
||||
<ErrorBoundary key={event.id} event={event}>
|
||||
<Zap zapEvent={event} />
|
||||
{zaps.map((zaps) => (
|
||||
<ErrorBoundary key={zaps.id} event={zaps}>
|
||||
<Zap zap={zaps} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
|
||||
|
@ -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 [];
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user