mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
Use Relay class from nostr-tools
upgrade nostr-tools from 2.1.3 to 2.4.0 improve parsing zaps
This commit is contained in:
parent
71b4c4e1eb
commit
781948afd6
5
.changeset/ninety-starfishes-teach.md
Normal file
5
.changeset/ninety-starfishes-teach.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Use Relay class from nostr-tools
|
@ -62,7 +62,7 @@
|
||||
"nanoid": "^5.0.4",
|
||||
"ngeohash": "^0.6.3",
|
||||
"nostr-idb": "^2.1.1",
|
||||
"nostr-tools": "^2.1.3",
|
||||
"nostr-tools": "^2.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -1,9 +1,8 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { Filter, NostrEvent, Relay, Subscription } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import debug, { Debugger } from "debug";
|
||||
|
||||
import NostrSubscription from "./nostr-subscription";
|
||||
import EventStore from "./event-store";
|
||||
import { getEventCoordinate } from "../helpers/nostr/event";
|
||||
|
||||
@ -15,20 +14,17 @@ const RELAY_REQUEST_BATCH_TIME = 500;
|
||||
|
||||
/** This class is ued to batch requests by kind to a single relay */
|
||||
export default class BatchKindLoader {
|
||||
private subscription: NostrSubscription;
|
||||
private subscription: Subscription | null = null;
|
||||
events = new EventStore();
|
||||
relay: Relay;
|
||||
|
||||
private requestNext = new Set<string>();
|
||||
private requested = new Map<string, Date>();
|
||||
|
||||
log: Debugger;
|
||||
|
||||
constructor(relay: string, log?: Debugger) {
|
||||
this.subscription = new NostrSubscription(relay, undefined, `replaceable-event-loader`);
|
||||
|
||||
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
|
||||
this.subscription.onEOSE.subscribe(this.handleEOSE.bind(this));
|
||||
|
||||
constructor(relay: Relay, log?: Debugger) {
|
||||
this.relay = relay;
|
||||
this.log = log || debug("RelayBatchLoader");
|
||||
}
|
||||
|
||||
@ -105,12 +101,17 @@ export default class BatchKindLoader {
|
||||
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
|
||||
.join(", "),
|
||||
);
|
||||
this.subscription.setFilters(query);
|
||||
|
||||
if (this.subscription.state !== NostrSubscription.OPEN) {
|
||||
this.subscription.open();
|
||||
if (!this.subscription || this.subscription.closed) {
|
||||
this.subscription = this.relay.subscribe(query, {
|
||||
onevent: (event) => this.handleEvent(event),
|
||||
oneose: () => this.handleEOSE(),
|
||||
});
|
||||
} else {
|
||||
this.subscription.filters = query;
|
||||
this.subscription.fire();
|
||||
}
|
||||
} else if (this.subscription.state === NostrSubscription.OPEN) {
|
||||
} else if (this.subscription && !this.subscription.closed) {
|
||||
this.subscription.close();
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { Debugger } from "debug";
|
||||
import { Filter, NostrEvent, matchFilters } from "nostr-tools";
|
||||
import { Filter, NostrEvent, Relay, matchFilters } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import NostrRequest from "./nostr-request";
|
||||
import Subject from "./subject";
|
||||
import { logger } from "../helpers/debug";
|
||||
import EventStore from "./event-store";
|
||||
import deleteEventService from "../services/delete-events";
|
||||
import { mergeFilter } from "../helpers/nostr/filter";
|
||||
import { isATag, isETag } from "../types/nostr-event";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 100;
|
||||
|
||||
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
|
||||
|
||||
export default class ChunkedRequest {
|
||||
relay: string;
|
||||
relay: Relay;
|
||||
filters: Filter[];
|
||||
chunkSize = DEFAULT_CHUNK_SIZE;
|
||||
private log: Debugger;
|
||||
@ -28,12 +28,12 @@ export default class ChunkedRequest {
|
||||
|
||||
onChunkFinish = new Subject<number>();
|
||||
|
||||
constructor(relay: string, filters: Filter[], log?: Debugger) {
|
||||
constructor(relay: Relay, filters: Filter[], log?: Debugger) {
|
||||
this.relay = relay;
|
||||
this.filters = filters;
|
||||
|
||||
this.log = log || logger.extend(relay);
|
||||
this.events = new EventStore(relay);
|
||||
this.log = log || logger.extend(relay.url);
|
||||
this.events = new EventStore(relay.url);
|
||||
|
||||
// TODO: find a better place for this
|
||||
this.subs.push(deleteEventService.stream.subscribe((e) => this.handleDeleteEvent(e)));
|
||||
@ -47,23 +47,25 @@ export default class ChunkedRequest {
|
||||
filters = mergeFilter(filters, { until: oldestEvent.created_at - 1 });
|
||||
}
|
||||
|
||||
const request = new NostrRequest([this.relay]);
|
||||
relayPoolService.addClaim(this.relay, this);
|
||||
|
||||
let gotEvents = 0;
|
||||
request.onEvent.subscribe((e) => {
|
||||
this.handleEvent(e);
|
||||
gotEvents++;
|
||||
const sub = this.relay.subscribe(filters, {
|
||||
onevent: (event) => {
|
||||
this.handleEvent(event);
|
||||
gotEvents++;
|
||||
},
|
||||
oneose: () => {
|
||||
this.loading = false;
|
||||
if (gotEvents === 0) {
|
||||
this.complete = true;
|
||||
this.log("Complete");
|
||||
} else this.log(`Got ${gotEvents} events`);
|
||||
this.onChunkFinish.next(gotEvents);
|
||||
sub.close();
|
||||
relayPoolService.removeClaim(this.relay, this);
|
||||
},
|
||||
});
|
||||
request.onComplete.then(() => {
|
||||
this.loading = false;
|
||||
if (gotEvents === 0) {
|
||||
this.complete = true;
|
||||
this.log("Complete");
|
||||
} else this.log(`Got ${gotEvents} events`);
|
||||
this.onChunkFinish.next(gotEvents);
|
||||
});
|
||||
|
||||
request.start(filters);
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-relay";
|
||||
import Relay, { IncomingEvent, OutgoingRequest } from "./relay";
|
||||
import { RelayQueryMap } from "../types/nostr-relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { isFilterEqual, isQueryMapEqual } from "../helpers/nostr/filter";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import SuperMap from "./super-map";
|
||||
import { Relay, Subscription } from "nostr-tools";
|
||||
|
||||
export default class NostrMultiSubscription {
|
||||
static INIT = "initial";
|
||||
@ -18,6 +17,8 @@ export default class NostrMultiSubscription {
|
||||
queryMap: RelayQueryMap = {};
|
||||
|
||||
relays: Relay[] = [];
|
||||
subscriptions = new Map<Relay, Subscription>();
|
||||
|
||||
state = NostrMultiSubscription.INIT;
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
seenEvents = new Set<string>();
|
||||
@ -26,38 +27,17 @@ export default class NostrMultiSubscription {
|
||||
this.id = nanoid();
|
||||
this.name = name;
|
||||
}
|
||||
private handleMessage(incomingEvent: IncomingEvent) {
|
||||
if (
|
||||
this.state === NostrMultiSubscription.OPEN &&
|
||||
incomingEvent[1] === this.id &&
|
||||
!this.seenEvents.has(incomingEvent[2].id)
|
||||
) {
|
||||
this.onEvent.next(incomingEvent[2]);
|
||||
this.seenEvents.add(incomingEvent[2].id);
|
||||
}
|
||||
private handleEvent(event: NostrEvent) {
|
||||
if (this.seenEvents.has(event.id)) return;
|
||||
this.onEvent.next(event);
|
||||
this.seenEvents.add(event.id);
|
||||
}
|
||||
|
||||
private relaySubs = new SuperMap<Relay, ZenObservable.Subscription[]>(() => []);
|
||||
/** listen for event and open events from relays */
|
||||
private connectToRelay(relay: Relay) {
|
||||
const subs = this.relaySubs.get(relay);
|
||||
subs.push(relay.onEvent.subscribe(this.handleMessage.bind(this)));
|
||||
subs.push(relay.onOpen.subscribe(this.handleRelayConnect.bind(this)));
|
||||
subs.push(relay.onClose.subscribe(this.handleRelayDisconnect.bind(this)));
|
||||
private handleAddRelay(relay: Relay) {
|
||||
relayPoolService.addClaim(relay.url, this);
|
||||
}
|
||||
/** stop listing to events from relays */
|
||||
private disconnectFromRelay(relay: Relay) {
|
||||
const subs = this.relaySubs.get(relay);
|
||||
for (const sub of subs) sub.unsubscribe();
|
||||
this.relaySubs.delete(relay);
|
||||
private handleRemoveRelay(relay: Relay) {
|
||||
relayPoolService.removeClaim(relay.url, this);
|
||||
|
||||
// if the subscription is open and had sent a request to the relay
|
||||
if (this.state === NostrMultiSubscription.OPEN && this.relayQueries.has(relay)) {
|
||||
relay.send(["CLOSE", this.id]);
|
||||
}
|
||||
this.relayQueries.delete(relay);
|
||||
}
|
||||
|
||||
setQueryMap(queryMap: RelayQueryMap) {
|
||||
@ -70,7 +50,7 @@ export default class NostrMultiSubscription {
|
||||
// add relay
|
||||
const relay = relayPoolService.requestRelay(url);
|
||||
this.relays.push(relay);
|
||||
this.connectToRelay(relay);
|
||||
this.handleAddRelay(relay);
|
||||
}
|
||||
}
|
||||
for (const url of Object.keys(this.queryMap)) {
|
||||
@ -78,41 +58,51 @@ export default class NostrMultiSubscription {
|
||||
const relay = this.relays.find((r) => r.url === url);
|
||||
if (!relay) continue;
|
||||
this.relays = this.relays.filter((r) => r !== relay);
|
||||
this.disconnectFromRelay(relay);
|
||||
this.handleRemoveRelay(relay);
|
||||
}
|
||||
}
|
||||
|
||||
this.queryMap = queryMap;
|
||||
|
||||
this.updateRelayQueries();
|
||||
this.updateSubscriptions();
|
||||
}
|
||||
|
||||
private relayQueries = new WeakMap<Relay, NostrRequestFilter>();
|
||||
private updateRelayQueries() {
|
||||
if (this.state !== NostrMultiSubscription.OPEN) return;
|
||||
private updateSubscriptions() {
|
||||
// close all subscriptions if not open
|
||||
if (this.state !== NostrMultiSubscription.OPEN) {
|
||||
for (const [relay, subscription] of this.subscriptions) {
|
||||
subscription.close();
|
||||
}
|
||||
this.subscriptions.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// else open and update subscriptions
|
||||
for (const relay of this.relays) {
|
||||
const filter = this.queryMap[relay.url];
|
||||
const message: OutgoingRequest = Array.isArray(filter) ? ["REQ", this.id, ...filter] : ["REQ", this.id, filter];
|
||||
const filters = this.queryMap[relay.url];
|
||||
|
||||
const currentFilter = this.relayQueries.get(relay);
|
||||
if (!currentFilter || !isFilterEqual(currentFilter, filter)) {
|
||||
this.relayQueries.set(relay, filter);
|
||||
relay.send(message);
|
||||
let subscription = this.subscriptions.get(relay);
|
||||
if (!subscription || !isFilterEqual(subscription.filters, filters)) {
|
||||
if (subscription) {
|
||||
subscription.filters = filters;
|
||||
subscription.fire();
|
||||
} else {
|
||||
subscription = relay.subscribe(filters, {
|
||||
onevent: (event) => this.handleEvent(event),
|
||||
onclose: () => {
|
||||
if (this.subscriptions.get(relay) === subscription) {
|
||||
this.subscriptions.delete(relay);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.subscriptions.set(relay, subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private handleRelayConnect(relay: Relay) {
|
||||
this.updateRelayQueries();
|
||||
}
|
||||
private handleRelayDisconnect(relay: Relay) {
|
||||
this.relayQueries.delete(relay);
|
||||
}
|
||||
|
||||
sendAll(event: NostrEvent) {
|
||||
for (const relay of this.relays) {
|
||||
relay.send(["EVENT", event]);
|
||||
}
|
||||
publish(event: NostrEvent) {
|
||||
return Promise.all(this.relays.map((r) => r.publish(event)));
|
||||
}
|
||||
|
||||
open() {
|
||||
@ -120,14 +110,14 @@ export default class NostrMultiSubscription {
|
||||
|
||||
this.state = NostrMultiSubscription.OPEN;
|
||||
// reconnect to all relays
|
||||
for (const relay of this.relays) this.connectToRelay(relay);
|
||||
for (const relay of this.relays) this.handleAddRelay(relay);
|
||||
// send queries
|
||||
this.updateRelayQueries();
|
||||
this.updateSubscriptions();
|
||||
|
||||
return this;
|
||||
}
|
||||
waitForConnection(): Promise<void> {
|
||||
return Promise.all(this.relays.map((r) => r.waitForConnection())).then((v) => void 0);
|
||||
waitForAllConnection(): Promise<void> {
|
||||
return Promise.all(this.relays.filter((r) => !r.connected).map((r) => r.connect())).then((v) => void 0);
|
||||
}
|
||||
close() {
|
||||
if (this.state !== NostrMultiSubscription.OPEN) return this;
|
||||
@ -135,7 +125,7 @@ export default class NostrMultiSubscription {
|
||||
// forget all seen events
|
||||
this.forgetEvents();
|
||||
// unsubscribe from relay messages
|
||||
for (const relay of this.relays) this.disconnectFromRelay(relay);
|
||||
for (const relay of this.relays) this.handleRemoveRelay(relay);
|
||||
// set state
|
||||
this.state = NostrMultiSubscription.CLOSED;
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { NostrEvent, Relay } from "nostr-tools";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import createDefer from "./deferred";
|
||||
import Relay, { IncomingCommandResult } from "./relay";
|
||||
import { PersistentSubject } from "./subject";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import SuperMap from "./super-map";
|
||||
|
||||
type Result = { relay: Relay; success: boolean; message: string };
|
||||
|
||||
export default class NostrPublishAction {
|
||||
id = nanoid();
|
||||
@ -14,13 +14,12 @@ export default class NostrPublishAction {
|
||||
relays: string[];
|
||||
event: NostrEvent;
|
||||
|
||||
results = new PersistentSubject<{ relay: Relay; result: IncomingCommandResult }[]>([]);
|
||||
results = new PersistentSubject<Result[]>([]);
|
||||
|
||||
onResult = new ControlledObservable<{ relay: Relay; result: IncomingCommandResult }>();
|
||||
onComplete = createDefer<{ relay: Relay; result: IncomingCommandResult }[]>();
|
||||
onResult = new ControlledObservable<Result>();
|
||||
onComplete = createDefer<Result[]>();
|
||||
|
||||
private remaining = new Set<Relay>();
|
||||
private relayResultSubs = new SuperMap<Relay, ZenObservable.Subscription[]>(() => []);
|
||||
|
||||
constructor(label: string, relays: Iterable<string>, event: NostrEvent, timeout: number = 5000) {
|
||||
this.label = label;
|
||||
@ -30,31 +29,30 @@ export default class NostrPublishAction {
|
||||
for (const url of relays) {
|
||||
const relay = relayPoolService.requestRelay(url);
|
||||
this.remaining.add(relay);
|
||||
this.relayResultSubs.get(relay).push(
|
||||
relay.onCommandResult.subscribe((result) => {
|
||||
if (result[1] === this.event.id) this.handleResult(result, relay);
|
||||
}),
|
||||
);
|
||||
|
||||
relay.send(["EVENT", event]);
|
||||
relay
|
||||
.publish(event)
|
||||
.then((result) => this.handleResult(event.id, true, result, relay))
|
||||
.catch((err) => {
|
||||
if (err instanceof Error) this.handleResult(event.id, false, err.message, relay);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(this.handleTimeout.bind(this), timeout);
|
||||
}
|
||||
|
||||
private handleResult(result: IncomingCommandResult, relay: Relay) {
|
||||
this.results.next([...this.results.value, { relay, result }]);
|
||||
this.onResult.next({ relay, result });
|
||||
private handleResult(id: string, success: boolean, message: string, relay: Relay) {
|
||||
const result: Result = { relay, success, message };
|
||||
this.results.next([...this.results.value, result]);
|
||||
this.onResult.next(result);
|
||||
|
||||
this.relayResultSubs.get(relay).forEach((s) => s.unsubscribe());
|
||||
this.relayResultSubs.delete(relay);
|
||||
this.remaining.delete(relay);
|
||||
if (this.remaining.size === 0) this.onComplete.resolve(this.results.value);
|
||||
}
|
||||
|
||||
private handleTimeout() {
|
||||
for (const relay of this.remaining) {
|
||||
this.handleResult(["OK", this.event.id, false, "Timeout"], relay);
|
||||
this.handleResult(this.event.id, false, "Timeout", relay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,95 +0,0 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import Relay, { CountResponse, IncomingCount, IncomingEOSE, IncomingEvent } from "./relay";
|
||||
import createDefer from "./deferred";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import SuperMap from "./super-map";
|
||||
|
||||
const REQUEST_DEFAULT_TIMEOUT = 1000 * 5;
|
||||
export default class NostrRequest {
|
||||
static IDLE = "idle";
|
||||
static RUNNING = "running";
|
||||
static COMPLETE = "complete";
|
||||
|
||||
id = nanoid();
|
||||
timeout: number;
|
||||
relays: Set<Relay>;
|
||||
state = NostrRequest.IDLE;
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
onCount = new ControlledObservable<CountResponse>();
|
||||
onComplete = createDefer<void>();
|
||||
seenEvents = new Set<string>();
|
||||
|
||||
private relaySubs: SuperMap<Relay, ZenObservable.Subscription[]> = new SuperMap(() => []);
|
||||
|
||||
constructor(relayUrls: Iterable<string>, timeout?: number) {
|
||||
this.relays = new Set(Array.from(relayUrls).map((url) => relayPoolService.requestRelay(url)));
|
||||
|
||||
for (const relay of this.relays) {
|
||||
const subs = this.relaySubs.get(relay);
|
||||
subs.push(relay.onEOSE.subscribe((m) => this.handleEOSE(m, relay)));
|
||||
subs.push(relay.onEvent.subscribe(this.handleEvent.bind(this)));
|
||||
subs.push(relay.onCount.subscribe(this.handleCount.bind(this)));
|
||||
}
|
||||
|
||||
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
handleEOSE(message: IncomingEOSE, relay: Relay) {
|
||||
if (message[1] === this.id) {
|
||||
this.relays.delete(relay);
|
||||
relay.send(["CLOSE", this.id]);
|
||||
|
||||
this.relaySubs.get(relay).forEach((sub) => sub.unsubscribe());
|
||||
this.relaySubs.delete(relay);
|
||||
|
||||
if (this.relays.size === 0) {
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
this.onComplete.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
handleEvent(message: IncomingEvent) {
|
||||
if (this.state === NostrRequest.RUNNING && message[1] === this.id && !this.seenEvents.has(message[2].id)) {
|
||||
this.onEvent.next(message[2]);
|
||||
this.seenEvents.add(message[2].id);
|
||||
}
|
||||
}
|
||||
handleCount(incomingCount: IncomingCount) {
|
||||
if (incomingCount[1] === this.id) {
|
||||
this.onCount.next(incomingCount[2]);
|
||||
}
|
||||
}
|
||||
|
||||
start(filter: Filter | Filter[], type: "REQ" | "COUNT" = "REQ") {
|
||||
if (this.state !== NostrRequest.IDLE) {
|
||||
throw new Error("cant restart a nostr request");
|
||||
}
|
||||
|
||||
this.state = NostrRequest.RUNNING;
|
||||
for (const relay of this.relays) {
|
||||
if (Array.isArray(filter)) {
|
||||
relay.send([type, this.id, ...filter]);
|
||||
} else relay.send([type, this.id, filter]);
|
||||
}
|
||||
|
||||
setTimeout(() => this.complete(), this.timeout);
|
||||
|
||||
return this;
|
||||
}
|
||||
complete() {
|
||||
if (this.state === NostrRequest.COMPLETE) return this;
|
||||
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
for (const relay of this.relays) {
|
||||
relay.send(["CLOSE", this.id]);
|
||||
this.relaySubs.get(relay).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
this.relaySubs.clear();
|
||||
this.onComplete.resolve();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { Filter, NostrEvent, Relay, Subscription } from "nostr-tools";
|
||||
|
||||
import Relay, { IncomingEOSE, OutgoingMessage } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
|
||||
/** @deprecated use relay.subscribe instead */
|
||||
export default class NostrSubscription {
|
||||
static INIT = "initial";
|
||||
static OPEN = "open";
|
||||
@ -16,10 +16,10 @@ export default class NostrSubscription {
|
||||
relay: Relay;
|
||||
state = NostrSubscription.INIT;
|
||||
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
onEOSE = new ControlledObservable<IncomingEOSE>();
|
||||
subscription: Subscription | null = null;
|
||||
|
||||
private subs: ZenObservable.Subscription[] = [];
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
onEOSE = new ControlledObservable<number>();
|
||||
|
||||
constructor(relayUrl: string | URL, filters?: Filter[], name?: string) {
|
||||
this.id = nanoid();
|
||||
@ -27,28 +27,13 @@ export default class NostrSubscription {
|
||||
this.name = name;
|
||||
|
||||
this.relay = relayPoolService.requestRelay(relayUrl);
|
||||
|
||||
this.subs.push(
|
||||
this.relay.onEvent.subscribe((message) => {
|
||||
if (this.state === NostrSubscription.OPEN && message[1] === this.id) {
|
||||
this.onEvent.next(message[2]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.subs.push(
|
||||
this.relay.onEOSE.subscribe((eose) => {
|
||||
if (this.state === NostrSubscription.OPEN && eose[1] === this.id) this.onEOSE.next(eose);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
send(message: OutgoingMessage) {
|
||||
this.relay.send(message);
|
||||
}
|
||||
setFilters(filters: Filter[]) {
|
||||
this.filters = filters;
|
||||
if (this.state === NostrSubscription.OPEN) {
|
||||
this.send(["REQ", this.id, ...this.filters]);
|
||||
if (this.state === NostrSubscription.OPEN && this.subscription) {
|
||||
this.subscription.filters = this.filters;
|
||||
this.subscription.fire();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@ -58,7 +43,10 @@ export default class NostrSubscription {
|
||||
if (this.state === NostrSubscription.OPEN) return this;
|
||||
|
||||
this.state = NostrSubscription.OPEN;
|
||||
this.send(["REQ", this.id, ...this.filters]);
|
||||
this.subscription = this.relay.subscribe(this.filters, {
|
||||
onevent: (event) => this.onEvent.next(event),
|
||||
oneose: () => this.onEOSE.next(Math.random()),
|
||||
});
|
||||
|
||||
relayPoolService.addClaim(this.relay.url, this);
|
||||
|
||||
@ -70,13 +58,10 @@ export default class NostrSubscription {
|
||||
// set state
|
||||
this.state = NostrSubscription.CLOSED;
|
||||
// send close message
|
||||
this.send(["CLOSE", this.id]);
|
||||
this.subscription?.close();
|
||||
// unsubscribe from relay messages
|
||||
relayPoolService.removeClaim(this.relay.url, this);
|
||||
|
||||
for (const sub of this.subs) sub.unsubscribe();
|
||||
this.subs = [];
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ export default class AccountNotifications {
|
||||
break;
|
||||
}
|
||||
case NotificationType.Zap:
|
||||
const parsed = getParsedZap(e);
|
||||
const parsed = getParsedZap(e, true, true);
|
||||
if (parsed instanceof Error) break;
|
||||
if (!parsed.payment.amount) break;
|
||||
timeline.push(e);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Relay } from "nostr-tools";
|
||||
import { logger } from "../helpers/debug";
|
||||
import { validateRelayURL } from "../helpers/relay";
|
||||
import { offlineMode } from "../services/offline-mode";
|
||||
import Relay from "./relay";
|
||||
import Subject from "./subject";
|
||||
|
||||
export default class RelayPool {
|
||||
@ -34,9 +34,9 @@ export default class RelayPool {
|
||||
}
|
||||
|
||||
const relay = this.relays.get(key) as Relay;
|
||||
if (connect && !relay.okay) {
|
||||
if (connect && !relay.connected) {
|
||||
try {
|
||||
relay.open();
|
||||
relay.connect();
|
||||
} catch (e) {
|
||||
this.log(`Failed to connect to ${relay.url}`);
|
||||
this.log(e);
|
||||
@ -58,9 +58,9 @@ export default class RelayPool {
|
||||
|
||||
for (const [url, relay] of this.relays.entries()) {
|
||||
const claims = this.getRelayClaims(url).size;
|
||||
if (!relay.okay && claims > 0) {
|
||||
if (!relay.connected && claims > 0) {
|
||||
try {
|
||||
relay.open();
|
||||
relay.connect();
|
||||
} catch (e) {
|
||||
this.log(`Failed to connect to ${relay.url}`);
|
||||
this.log(e);
|
||||
@ -69,14 +69,12 @@ export default class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
addClaim(url: string | URL, id: any) {
|
||||
url = validateRelayURL(url);
|
||||
const key = url.toString();
|
||||
addClaim(relay: string | URL | Relay, id: any) {
|
||||
const key = relay instanceof Relay ? relay.url : validateRelayURL(relay).toString();
|
||||
this.getRelayClaims(key).add(id);
|
||||
}
|
||||
removeClaim(url: string | URL, id: any) {
|
||||
url = validateRelayURL(url);
|
||||
const key = url.toString();
|
||||
removeClaim(relay: string | URL | Relay, id: any) {
|
||||
const key = relay instanceof Relay ? relay.url : validateRelayURL(relay).toString();
|
||||
this.getRelayClaims(key).delete(id);
|
||||
}
|
||||
|
||||
|
@ -1,211 +1,6 @@
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { offlineMode } from "../services/offline-mode";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import createDefer, { Deferred } from "./deferred";
|
||||
import { PersistentSubject } from "./subject";
|
||||
|
||||
export type CountResponse = {
|
||||
count: number;
|
||||
approximate?: boolean;
|
||||
};
|
||||
|
||||
export type IncomingEvent = ["EVENT", string, NostrEvent];
|
||||
export type IncomingNotice = ["NOTICE", string];
|
||||
export type IncomingCount = ["COUNT", string, CountResponse];
|
||||
export type IncomingEOSE = ["EOSE", string];
|
||||
export type IncomingCommandResult = ["OK", string, boolean] | ["OK", string, boolean, string];
|
||||
export type IncomingMessage = IncomingEvent | IncomingNotice | IncomingCount | IncomingEOSE | IncomingCommandResult;
|
||||
|
||||
export type OutgoingEvent = ["EVENT", NostrEvent];
|
||||
export type OutgoingRequest = ["REQ", string, ...Filter[]];
|
||||
export type OutgoingCount = ["COUNT", string, ...Filter[]];
|
||||
export type OutgoingClose = ["CLOSE", string];
|
||||
export type OutgoingMessage = OutgoingEvent | OutgoingRequest | OutgoingClose | OutgoingCount;
|
||||
|
||||
export enum RelayMode {
|
||||
NONE = 0,
|
||||
READ = 1,
|
||||
WRITE = 2,
|
||||
ALL = 1 | 2,
|
||||
}
|
||||
|
||||
const CONNECTION_TIMEOUT = 1000 * 30;
|
||||
|
||||
export default class Relay {
|
||||
url: string;
|
||||
ws?: WebSocket;
|
||||
status = new PersistentSubject<number>(WebSocket.CLOSED);
|
||||
onOpen = new ControlledObservable<Relay>();
|
||||
onClose = new ControlledObservable<Relay>();
|
||||
|
||||
onEvent = new ControlledObservable<IncomingEvent>();
|
||||
onNotice = new ControlledObservable<IncomingNotice>();
|
||||
onCount = new ControlledObservable<IncomingCount>();
|
||||
onEOSE = new ControlledObservable<IncomingEOSE>();
|
||||
onCommandResult = new ControlledObservable<IncomingCommandResult>();
|
||||
|
||||
private connectionPromises: Deferred<void>[] = [];
|
||||
|
||||
private connectionTimer?: () => void;
|
||||
private ejectTimer?: () => void;
|
||||
private intentionalClose = false;
|
||||
private subscriptionResTimer = new Map<string, () => void>();
|
||||
private queue: OutgoingMessage[] = [];
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
open() {
|
||||
if (offlineMode.value) return;
|
||||
|
||||
if (this.okay) return;
|
||||
this.intentionalClose = false;
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.connectionTimer = relayScoreboardService.relayConnectionTime.get(this.url).createTimer();
|
||||
const connectionTimeout: number = window.setTimeout(() => {
|
||||
// end the connection timer after CONNECTION_TIMEOUT
|
||||
if (this.connectionTimer) {
|
||||
this.connectionTimer();
|
||||
this.connectionTimer = undefined;
|
||||
|
||||
for (const p of this.connectionPromises) p.reject();
|
||||
this.connectionPromises = [];
|
||||
}
|
||||
// relayScoreboardService.relayTimeouts.get(this.url).addIncident();
|
||||
}, CONNECTION_TIMEOUT);
|
||||
|
||||
// for local dev, cancel timeout if module reloads
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.prune(() => {
|
||||
window.clearTimeout(connectionTimeout);
|
||||
this.ws?.close();
|
||||
});
|
||||
}
|
||||
|
||||
this.ws.onopen = () => {
|
||||
window.clearTimeout(connectionTimeout);
|
||||
this.onOpen.next(this);
|
||||
this.status.next(this.ws!.readyState);
|
||||
|
||||
this.ejectTimer = relayScoreboardService.relayEjectTime.get(this.url).createTimer();
|
||||
if (this.connectionTimer) {
|
||||
this.connectionTimer();
|
||||
this.connectionTimer = undefined;
|
||||
}
|
||||
|
||||
this.sendQueued();
|
||||
|
||||
for (const p of this.connectionPromises) p.resolve();
|
||||
this.connectionPromises = [];
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.onClose.next(this);
|
||||
this.status.next(this.ws!.readyState);
|
||||
|
||||
if (!this.intentionalClose && this.ejectTimer) {
|
||||
this.ejectTimer();
|
||||
this.ejectTimer = undefined;
|
||||
}
|
||||
};
|
||||
this.ws.onmessage = this.handleMessage.bind(this);
|
||||
}
|
||||
send(json: OutgoingMessage) {
|
||||
if (this.connected) {
|
||||
this.ws?.send(JSON.stringify(json));
|
||||
|
||||
// record start time
|
||||
if (json[0] === "REQ" || json[0] === "COUNT") {
|
||||
this.startSubResTimer(json[1]);
|
||||
}
|
||||
} else this.queue.push(json);
|
||||
}
|
||||
close() {
|
||||
this.ws?.close();
|
||||
this.intentionalClose = true;
|
||||
this.subscriptionResTimer.clear();
|
||||
}
|
||||
|
||||
waitForConnection(): Promise<void> {
|
||||
if (this.connected) return Promise.resolve();
|
||||
const p = createDefer<void>();
|
||||
this.connectionPromises.push(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
private startSubResTimer(sub: string) {
|
||||
this.subscriptionResTimer.set(sub, relayScoreboardService.relayResponseTimes.get(this.url).createTimer());
|
||||
}
|
||||
private endSubResTimer(sub: string) {
|
||||
const endTimer = this.subscriptionResTimer.get(sub);
|
||||
if (endTimer) {
|
||||
endTimer();
|
||||
this.subscriptionResTimer.delete(sub);
|
||||
}
|
||||
}
|
||||
|
||||
private sendQueued() {
|
||||
if (this.connected) {
|
||||
for (const message of this.queue) {
|
||||
this.send(message);
|
||||
}
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
|
||||
get okay() {
|
||||
return this.connected || this.connecting;
|
||||
}
|
||||
get connected() {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
get connecting() {
|
||||
return this.ws?.readyState === WebSocket.CONNECTING;
|
||||
}
|
||||
get closing() {
|
||||
return this.ws?.readyState === WebSocket.CLOSING;
|
||||
}
|
||||
get closed() {
|
||||
return this.ws?.readyState === WebSocket.CLOSED;
|
||||
}
|
||||
get state() {
|
||||
return this.ws?.readyState;
|
||||
}
|
||||
|
||||
handleMessage(message: MessageEvent<string>) {
|
||||
if (!message.data) return;
|
||||
|
||||
try {
|
||||
const data: IncomingMessage = JSON.parse(message.data);
|
||||
const type = data[0];
|
||||
|
||||
// all messages must have an argument
|
||||
if (!data[1]) return;
|
||||
|
||||
switch (type) {
|
||||
case "EVENT":
|
||||
this.onEvent.next(data);
|
||||
this.endSubResTimer(data[1]);
|
||||
break;
|
||||
case "NOTICE":
|
||||
this.onNotice.next(data);
|
||||
break;
|
||||
case "COUNT":
|
||||
this.onCount.next(data);
|
||||
break;
|
||||
case "EOSE":
|
||||
this.onEOSE.next(data);
|
||||
this.endSubResTimer(data[1]);
|
||||
break;
|
||||
case "OK":
|
||||
this.onCommandResult.next(data);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Relay: Failed to parse massage from ${this.url}`);
|
||||
console.log(message.data, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { localRelay } from "../services/local-relay";
|
||||
import { relayRequest } from "../helpers/relay";
|
||||
import SuperMap from "./super-map";
|
||||
import ChunkedRequest from "./chunked-request";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
|
||||
const BLOCK_SIZE = 100;
|
||||
|
||||
@ -124,7 +125,11 @@ export default class TimelineLoader {
|
||||
}
|
||||
|
||||
if (!this.chunkLoaders.has(relay)) {
|
||||
const loader = new ChunkedRequest(relay, Array.isArray(filter) ? filter : [filter], this.log.extend(relay));
|
||||
const loader = new ChunkedRequest(
|
||||
relayPoolService.requestRelay(relay),
|
||||
Array.isArray(filter) ? filter : [filter],
|
||||
this.log.extend(relay),
|
||||
);
|
||||
this.chunkLoaders.set(relay, loader);
|
||||
this.connectToChunkLoader(loader);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { MouseEventHandler, MutableRefObject, forwardRef, useCallback, useMemo, useRef } from "react";
|
||||
import { Image, ImageProps, Link, LinkProps } from "@chakra-ui/react";
|
||||
|
||||
import { useTrustContext } from "../../providers/local/trust";
|
||||
import { EmbedableContent, defaultGetLocation } from "../../helpers/embeds";
|
||||
import { getMatchLink } from "../../helpers/regexp";
|
||||
import { useRegisterSlide } from "../lightbox-provider";
|
||||
@ -100,7 +99,7 @@ export const GalleryImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(
|
||||
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
|
||||
const photos = useMemo(() => {
|
||||
return images.map((img) => {
|
||||
const photo: PhotoWithoutSize = { src: img };
|
||||
const photo: PhotoWithoutSize = { src: img, key: img };
|
||||
return photo;
|
||||
});
|
||||
}, [images]);
|
||||
|
@ -17,8 +17,8 @@ export function PublishDetails({ pub }: PostResultsProps & Omit<FlexProps, "chil
|
||||
<Flex direction="column" gap="2">
|
||||
<EmbedEvent event={pub.event} />
|
||||
<Progress value={(results.length / pub.relays.length) * 100} size="lg" hasStripe />
|
||||
{results.map(({ result, relay }) => (
|
||||
<Alert key={relay.url} status={result[2] ? "success" : "warning"}>
|
||||
{results.map(({ success, message, relay }) => (
|
||||
<Alert key={relay.url} status={success ? "success" : "warning"}>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>
|
||||
@ -27,7 +27,7 @@ export function PublishDetails({ pub }: PostResultsProps & Omit<FlexProps, "chil
|
||||
</Link>
|
||||
<RelayPaidTag url={relay.url} />
|
||||
</AlertTitle>
|
||||
{result[3] && <AlertDescription>{result[3]}</AlertDescription>}
|
||||
{message && <AlertDescription>{message}</AlertDescription>}
|
||||
</Box>
|
||||
</Alert>
|
||||
))}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useContext } from "react";
|
||||
import {
|
||||
Flex,
|
||||
FlexProps,
|
||||
@ -19,14 +20,13 @@ import NostrPublishAction from "../classes/nostr-publish-action";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import { CheckIcon, ErrorIcon } from "./icons";
|
||||
import { PublishDetails } from "./publish-details";
|
||||
import { useContext } from "react";
|
||||
import { PublishContext } from "../providers/global/publish-provider";
|
||||
|
||||
export function PublishActionStatusTag({ pub, ...props }: { pub: NostrPublishAction } & Omit<TagProps, "children">) {
|
||||
const results = useSubject(pub.results);
|
||||
|
||||
const successful = results.filter(({ result }) => result[2]);
|
||||
const failedWithMessage = results.filter(({ result }) => !result[2] && result[3]);
|
||||
const successful = results.filter(({ success }) => success);
|
||||
const failedWithMessage = results.filter(({ success, message }) => !success && !!message);
|
||||
|
||||
let statusIcon = <Spinner size="xs" />;
|
||||
let statusColor: TagProps["colorScheme"] = "blue";
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
DrawerOverlay,
|
||||
DrawerProps,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Select,
|
||||
@ -36,21 +35,9 @@ import { SaveRelaySetForm } from "./save-relay-set-form";
|
||||
|
||||
function RelayControl({ url }: { url: string }) {
|
||||
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
|
||||
const status = useSubject(relay.status);
|
||||
const writeRelays = useSubject(clientRelaysService.writeRelays);
|
||||
|
||||
let color = "gray";
|
||||
switch (status) {
|
||||
case WebSocket.OPEN:
|
||||
color = "green";
|
||||
break;
|
||||
case WebSocket.CONNECTING:
|
||||
color = "yellow";
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
color = "red";
|
||||
break;
|
||||
}
|
||||
const color = relay.connected ? "green" : "red";
|
||||
|
||||
const onChange = () => {
|
||||
if (writeRelays.has(url)) clientRelaysService.removeRelay(url, RelayMode.WRITE);
|
||||
@ -117,7 +104,7 @@ export default function RelayManagementDrawer({ isOpen, onClose, ...props }: Omi
|
||||
|
||||
const sorted = useMemo(() => RelaySet.from(readRelays, writeRelays).urls.sort(), [readRelays, writeRelays]);
|
||||
const others = Array.from(relayPoolService.relays.values())
|
||||
.filter((r) => !r.closed && !sorted.includes(r.url))
|
||||
.filter((r) => !r.connected && !sorted.includes(r.url))
|
||||
.map((r) => r.url)
|
||||
.sort();
|
||||
|
||||
|
@ -1,22 +1,24 @@
|
||||
import { Badge, useForceUpdate } from "@chakra-ui/react";
|
||||
import { useInterval } from "react-use";
|
||||
|
||||
import Relay from "../classes/relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { Relay } from "nostr-tools";
|
||||
|
||||
const getStatusText = (relay: Relay) => {
|
||||
if (relay.connecting) return "Connecting...";
|
||||
// if (relay.connecting) return "Connecting...";
|
||||
if (relay.connected) return "Connected";
|
||||
if (relay.closing) return "Disconnecting...";
|
||||
if (relay.closed) return "Disconnected";
|
||||
return "Unused";
|
||||
// if (relay.closing) return "Disconnecting...";
|
||||
// if (relay.closed) return "Disconnected";
|
||||
return "Disconnected";
|
||||
// return "Unused";
|
||||
};
|
||||
const getStatusColor = (relay: Relay) => {
|
||||
if (relay.connecting) return "yellow";
|
||||
// if (relay.connecting) return "yellow";
|
||||
if (relay.connected) return "green";
|
||||
if (relay.closing) return "yellow";
|
||||
if (relay.closed) return "red";
|
||||
return "gray";
|
||||
// if (relay.closing) return "yellow";
|
||||
// if (relay.closed) return "red";
|
||||
// return "gray";
|
||||
return "red";
|
||||
};
|
||||
|
||||
export const RelayStatus = ({ url }: { url: string }) => {
|
||||
|
@ -54,18 +54,38 @@ export type ParsedZap = {
|
||||
|
||||
const parsedZapSymbol = Symbol("parsedZap");
|
||||
type ParsedZapEvent = NostrEvent & { [parsedZapSymbol]: ParsedZap | Error };
|
||||
export function getParsedZap(event: NostrEvent) {
|
||||
|
||||
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)) return e[parsedZapSymbol];
|
||||
|
||||
try {
|
||||
return (e[parsedZapSymbol] = parseZapEvent(e));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return (e[parsedZapSymbol] = error);
|
||||
else throw 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");
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { SimpleRelay, SubscriptionOptions } from "nostr-idb";
|
||||
import { Filter } from "nostr-tools";
|
||||
import { AbstractRelay, Filter, SubCloser, SubscribeManyParams, Subscription } from "nostr-tools";
|
||||
|
||||
import { NostrQuery, NostrRequestFilter } from "../types/nostr-relay";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
|
||||
// NOTE: only use this for equality checks and querying
|
||||
export function getRelayVariations(relay: string) {
|
||||
@ -110,3 +111,81 @@ export function relayRequest(relay: SimpleRelay, filters: Filter[], opts: Subscr
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// copied from nostr-tools, SimplePool#subscribeMany
|
||||
export function subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
||||
const _knownIds = new Set<string>();
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
// batch all EOSEs into a single
|
||||
const eosesReceived: boolean[] = [];
|
||||
let handleEose = (i: number) => {
|
||||
eosesReceived[i] = true;
|
||||
if (eosesReceived.filter((a) => a).length === relays.length) {
|
||||
params.oneose?.();
|
||||
handleEose = () => {};
|
||||
}
|
||||
};
|
||||
// batch all closes into a single
|
||||
const closesReceived: string[] = [];
|
||||
let handleClose = (i: number, reason: string) => {
|
||||
handleEose(i);
|
||||
closesReceived[i] = reason;
|
||||
if (closesReceived.filter((a) => a).length === relays.length) {
|
||||
params.onclose?.(closesReceived);
|
||||
handleClose = () => {};
|
||||
}
|
||||
};
|
||||
|
||||
const localAlreadyHaveEventHandler = (id: string) => {
|
||||
if (params.alreadyHaveEvent?.(id)) {
|
||||
return true;
|
||||
}
|
||||
const have = _knownIds.has(id);
|
||||
_knownIds.add(id);
|
||||
return have;
|
||||
};
|
||||
|
||||
// open a subscription in all given relays
|
||||
const allOpened = Promise.all(
|
||||
relays.map(validateRelayURL).map(async (url, i, arr) => {
|
||||
if (arr.indexOf(url) !== i) {
|
||||
// duplicate
|
||||
handleClose(i, "duplicate url");
|
||||
return;
|
||||
}
|
||||
|
||||
let relay: AbstractRelay;
|
||||
try {
|
||||
relay = relayPoolService.requestRelay(url);
|
||||
await relay.connect();
|
||||
// changed from nostr-tools
|
||||
// relay = await this.ensureRelay(url, {
|
||||
// connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,
|
||||
// });
|
||||
} catch (err) {
|
||||
handleClose(i, (err as any)?.message || String(err));
|
||||
return;
|
||||
}
|
||||
|
||||
let subscription = relay.subscribe(filters, {
|
||||
...params,
|
||||
oneose: () => handleEose(i),
|
||||
onclose: (reason) => handleClose(i, reason),
|
||||
alreadyHaveEvent: localAlreadyHaveEventHandler,
|
||||
eoseTimeout: params.maxWait,
|
||||
});
|
||||
|
||||
subs.push(subscription);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
async close() {
|
||||
await allOpened;
|
||||
subs.forEach((sub) => {
|
||||
sub.close();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useMemo } from "react";
|
||||
import eventZapsService from "../services/event-zaps";
|
||||
import { useReadRelays } from "./use-client-relays";
|
||||
import useSubject from "./use-subject";
|
||||
import { parseZapEvent } from "../helpers/nostr/zaps";
|
||||
import { getParsedZap } from "../helpers/nostr/zaps";
|
||||
|
||||
export default function useEventZaps(eventUID: string, additionalRelays?: Iterable<string>, alwaysRequest = true) {
|
||||
const readRelays = useReadRelays(additionalRelays);
|
||||
@ -18,9 +18,8 @@ export default function useEventZaps(eventUID: string, additionalRelays?: Iterab
|
||||
const zaps = useMemo(() => {
|
||||
const parsed = [];
|
||||
for (const zap of events) {
|
||||
try {
|
||||
parsed.push(parseZapEvent(zap));
|
||||
} catch (e) {}
|
||||
const p = getParsedZap(zap);
|
||||
if (p) parsed.push(p);
|
||||
}
|
||||
return parsed;
|
||||
}, [events]);
|
||||
|
@ -4,8 +4,9 @@ import { GOAL_KIND } from "../helpers/nostr/goal";
|
||||
import { ParsedStream, getATag } from "../helpers/nostr/stream";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { useReadRelays } from "./use-client-relays";
|
||||
import NostrRequest from "../classes/nostr-request";
|
||||
import useSingleEvent from "./use-single-event";
|
||||
import { subscribeMany } from "../helpers/relay";
|
||||
import { Filter } from "nostr-tools";
|
||||
|
||||
export default function useStreamGoal(stream: ParsedStream) {
|
||||
const [goal, setGoal] = useState<NostrEvent>();
|
||||
@ -15,11 +16,11 @@ export default function useStreamGoal(stream: ParsedStream) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!stream.goal) {
|
||||
const request = new NostrRequest(readRelays);
|
||||
request.onEvent.subscribe((event) => {
|
||||
setGoal(event);
|
||||
const filter: Filter = { "#a": [getATag(stream)], kinds: [GOAL_KIND] };
|
||||
const sub = subscribeMany(Array.from(readRelays), [filter], {
|
||||
onevent: (event) => setGoal((c) => (!c || event.created_at > c.created_at ? event : c)),
|
||||
oneose: () => sub.close(),
|
||||
});
|
||||
request.start({ "#a": [getATag(stream)], kinds: [GOAL_KIND] });
|
||||
}
|
||||
}, [stream.identifier, stream.goal, readRelays.urls.join("|")]);
|
||||
|
||||
|
@ -66,8 +66,8 @@ export default function PublishProvider({ children }: PropsWithChildren) {
|
||||
const pub = new NostrPublishAction(label, relays, signed);
|
||||
setLog((arr) => arr.concat(pub));
|
||||
|
||||
pub.onResult.subscribe(({ relay, result }) => {
|
||||
if (result[2]) handleEventFromRelay(relay, signed);
|
||||
pub.onResult.subscribe(({ relay, success }) => {
|
||||
if (success) handleEventFromRelay(relay, signed);
|
||||
});
|
||||
|
||||
// send it to the local relay
|
||||
|
@ -40,7 +40,7 @@ class DnsIdentityService {
|
||||
|
||||
async fetchIdentity(address: string) {
|
||||
const { name, domain } = parseAddress(address);
|
||||
if (!name || !domain) throw new Error("invalid address");
|
||||
if (!name || !domain) throw new Error("invalid address " + address);
|
||||
|
||||
const json = await fetchWithCorsFallback(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||
.then((res) => res.json() as Promise<IdentityJson>)
|
||||
|
@ -3,13 +3,13 @@ import stringify from "json-stringify-deterministic";
|
||||
import Subject from "../classes/subject";
|
||||
import { NostrRequestFilter } from "../types/nostr-relay";
|
||||
import SuperMap from "../classes/super-map";
|
||||
import NostrRequest from "../classes/nostr-request";
|
||||
import relayScoreboardService from "./relay-scoreboard";
|
||||
import { logger } from "../helpers/debug";
|
||||
import { matchFilter, matchFilters } from "nostr-tools";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { relayRequest } from "../helpers/relay";
|
||||
import { localRelay } from "./local-relay";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
function hashFilter(filter: NostrRequestFilter) {
|
||||
return stringify(filter);
|
||||
@ -62,20 +62,23 @@ class EventExistsService {
|
||||
relays.delete(nextRelay);
|
||||
|
||||
(async () => {
|
||||
const sub = this.answers.get(key);
|
||||
const request = new NostrRequest([nextRelay], 500);
|
||||
const limitFilter = Array.isArray(filter) ? filter.map((f) => ({ ...f, limit: 1 })) : { ...filter, limit: 1 };
|
||||
request.start(limitFilter);
|
||||
request.onEvent.subscribe(() => {
|
||||
this.log("Found event for", filter);
|
||||
sub.next(true);
|
||||
this.pending.delete(key);
|
||||
const subject = this.answers.get(key);
|
||||
const limitFilter = Array.isArray(filter) ? filter.map((f) => ({ ...f, limit: 1 })) : [{ ...filter, limit: 1 }];
|
||||
const subscription = relayPoolService.requestRelay(nextRelay).subscribe(limitFilter, {
|
||||
eoseTimeout: 500,
|
||||
onevent: () => {
|
||||
this.log("Found event for", filter);
|
||||
subject.next(true);
|
||||
this.pending.delete(key);
|
||||
},
|
||||
oneose: () => {
|
||||
if (subject.value === undefined && this.asked.get(key).size > this.pending.get(key).size) {
|
||||
this.log("Could not find event for", filter);
|
||||
subject.next(false);
|
||||
}
|
||||
subscription.close();
|
||||
},
|
||||
});
|
||||
await request.onComplete;
|
||||
if (sub.value === undefined && this.asked.get(key).size > this.pending.get(key).size) {
|
||||
this.log("Could not find event for", filter);
|
||||
sub.next(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Filter, kinds, nip25 } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import NostrRequest from "../classes/nostr-request";
|
||||
import Subject from "../classes/subject";
|
||||
import SuperMap from "../classes/super-map";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { localRelay } from "./local-relay";
|
||||
import { relayRequest } from "../helpers/relay";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
type eventId = string;
|
||||
type relay = string;
|
||||
@ -73,9 +73,9 @@ class EventReactionsService {
|
||||
if (coordinates.length > 0) filters.push({ "#a": coordinates, kinds: [kinds.Reaction] });
|
||||
|
||||
if (filters.length > 0) {
|
||||
const request = new NostrRequest([relay]);
|
||||
request.onEvent.subscribe((e) => this.handleEvent(e));
|
||||
request.start(filters);
|
||||
const subscription = relayPoolService
|
||||
.requestRelay(relay)
|
||||
.subscribe(filters, { onevent: (event) => this.handleEvent(event), oneose: () => subscription.close() });
|
||||
}
|
||||
}
|
||||
this.pending.clear();
|
||||
|
@ -1,8 +1,7 @@
|
||||
import Relay from "../classes/relay";
|
||||
import { Relay } from "nostr-tools";
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import { getEventUID } from "../helpers/nostr/event";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
const eventRelays = new Map<string, PersistentSubject<string[]>>();
|
||||
|
||||
@ -30,11 +29,12 @@ export function handleEventFromRelay(relay: Relay, event: NostrEvent) {
|
||||
if (event.id !== uid) addRelay(event.id, relay.url);
|
||||
}
|
||||
|
||||
relayPoolService.onRelayCreated.subscribe((relay) => {
|
||||
relay.onEvent.subscribe((message) => {
|
||||
handleEventFromRelay(relay, message[2]);
|
||||
});
|
||||
});
|
||||
// TODO: track events from relays
|
||||
// relayPoolService.onRelayCreated.subscribe((relay) => {
|
||||
// relay.onEvent.subscribe((message) => {
|
||||
// handleEventFromRelay(relay, message[2]);
|
||||
// });
|
||||
// });
|
||||
|
||||
const eventRelaysService = {
|
||||
getEventRelays,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Filter, kinds } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import NostrRequest from "../classes/nostr-request";
|
||||
import Subject from "../classes/subject";
|
||||
import SuperMap from "../classes/super-map";
|
||||
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
|
||||
import { relayRequest } from "../helpers/relay";
|
||||
import { localRelay } from "./local-relay";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
type eventUID = string;
|
||||
type relay = string;
|
||||
@ -73,9 +73,9 @@ class EventZapsService {
|
||||
if (coordinates.length > 0) filter.push({ "#a": coordinates, kinds: [kinds.Zap] });
|
||||
|
||||
if (filter.length > 0) {
|
||||
const request = new NostrRequest([relay]);
|
||||
request.onEvent.subscribe((e) => this.handleEvent(e));
|
||||
request.start(filter);
|
||||
const sub = relayPoolService
|
||||
.requestRelay(relay)
|
||||
.subscribe(filter, { onevent: (event) => this.handleEvent(event), oneose: () => sub.close() });
|
||||
}
|
||||
}
|
||||
this.pending.clear();
|
||||
|
@ -57,7 +57,8 @@ export type NostrConnectErrorResponse = {
|
||||
};
|
||||
|
||||
// FIXME list all requested perms
|
||||
const Perms = "nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:3,sign_event:4,sign_event:6,sign_event:7"
|
||||
const Perms =
|
||||
"nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:3,sign_event:4,sign_event:6,sign_event:7";
|
||||
|
||||
export class NostrConnectClient {
|
||||
sub: NostrMultiSubscription;
|
||||
@ -93,7 +94,7 @@ export class NostrConnectClient {
|
||||
|
||||
async open() {
|
||||
this.sub.open();
|
||||
await this.sub.waitForConnection();
|
||||
await this.sub.waitForAllConnection();
|
||||
this.log("Connected to relays", this.relays);
|
||||
}
|
||||
close() {
|
||||
@ -126,7 +127,7 @@ export class NostrConnectClient {
|
||||
this.log("Got Error", response.id, response.result, response.error);
|
||||
if (response.result === "auth_url") {
|
||||
if (!this.auths.has(response.id)) {
|
||||
this.auths.add(response.id)
|
||||
this.auths.add(response.id);
|
||||
try {
|
||||
await this.handleAuthURL(response.error);
|
||||
} catch (e) {
|
||||
@ -163,7 +164,7 @@ export class NostrConnectClient {
|
||||
const encrypted = await nip04.encrypt(this.secretKey, this.pubkey, JSON.stringify(request));
|
||||
const event = this.createEvent(encrypted, this.pubkey, kind);
|
||||
this.log(`Sending request ${id} (${method}) ${JSON.stringify(params)}`, event);
|
||||
this.sub.sendAll(event);
|
||||
this.sub.publish(event);
|
||||
|
||||
const p = createDefer<ResponseResults[T]>();
|
||||
this.requests.set(id, p);
|
||||
@ -180,7 +181,7 @@ export class NostrConnectClient {
|
||||
const encrypted = await nip04.encrypt(this.secretKey, this.provider, JSON.stringify(request));
|
||||
const event = this.createEvent(encrypted, this.provider, kind);
|
||||
this.log(`Sending admin request ${id} (${method}) ${JSON.stringify(params)}`, event);
|
||||
this.sub.sendAll(event);
|
||||
this.sub.publish(event);
|
||||
|
||||
const p = createDefer<ResponseResults[T]>();
|
||||
this.requests.set(id, p);
|
||||
@ -190,10 +191,7 @@ export class NostrConnectClient {
|
||||
async connect(token?: string) {
|
||||
await this.open();
|
||||
try {
|
||||
const result = await this.makeRequest(
|
||||
NostrConnectMethod.Connect,
|
||||
[this.pubkey, token || '', Perms],
|
||||
);
|
||||
const result = await this.makeRequest(NostrConnectMethod.Connect, [this.pubkey, token || "", Perms]);
|
||||
this.isConnected = true;
|
||||
return result;
|
||||
} catch (e) {
|
||||
@ -207,10 +205,12 @@ export class NostrConnectClient {
|
||||
await this.open();
|
||||
|
||||
try {
|
||||
const newPubkey = await this.makeAdminRequest(
|
||||
NostrConnectMethod.CreateAccount,
|
||||
[name, domain, email || '', Perms],
|
||||
);
|
||||
const newPubkey = await this.makeAdminRequest(NostrConnectMethod.CreateAccount, [
|
||||
name,
|
||||
domain,
|
||||
email || "",
|
||||
Perms,
|
||||
]);
|
||||
this.pubkey = newPubkey;
|
||||
this.isConnected = true;
|
||||
return newPubkey;
|
||||
|
@ -1,12 +1,13 @@
|
||||
import _throttle from "lodash/throttle";
|
||||
|
||||
import NostrRequest from "../classes/nostr-request";
|
||||
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";
|
||||
|
||||
const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f";
|
||||
const MONITOR_RELAY = "wss://history.nostr.watch";
|
||||
@ -50,9 +51,10 @@ class RelayStatsService {
|
||||
relayInfoService.getInfo(relay).then((info) => {
|
||||
if (!info.pubkey) return sub.next(null);
|
||||
|
||||
const request = new NostrRequest([relay, MONITOR_RELAY]);
|
||||
request.onEvent.subscribe((e) => this.handleEvent(e));
|
||||
request.start({ kinds: [SELF_REPORTED_KIND], authors: [info.pubkey] });
|
||||
const filter: Filter = { kinds: [SELF_REPORTED_KIND], authors: [info.pubkey] };
|
||||
const subscription = relayPoolService
|
||||
.requestRelay(MONITOR_RELAY)
|
||||
.subscribe([filter], { onevent: (event) => this.handleEvent(event), oneose: () => subscription.close() });
|
||||
});
|
||||
}
|
||||
|
||||
@ -74,9 +76,10 @@ class RelayStatsService {
|
||||
private batchRequestMonitorStats() {
|
||||
const relays = Array.from(this.pendingMonitorStats);
|
||||
|
||||
const request = new NostrRequest([MONITOR_RELAY]);
|
||||
request.onEvent.subscribe((e) => this.handleEvent(e));
|
||||
request.start({ since: 1704196800, kinds: [MONITOR_STATS_KIND], "#d": relays, authors: [MONITOR_PUBKEY] });
|
||||
const filter: Filter = { since: 1704196800, kinds: [MONITOR_STATS_KIND], "#d": relays, authors: [MONITOR_PUBKEY] };
|
||||
const sub = relayPoolService
|
||||
.requestRelay(MONITOR_RELAY)
|
||||
.subscribe([filter], { onevent: (event) => this.handleEvent(event), oneose: () => sub.close() });
|
||||
|
||||
this.pendingMonitorStats.clear();
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { relayRequest } from "../helpers/relay";
|
||||
import EventStore from "../classes/event-store";
|
||||
import Subject from "../classes/subject";
|
||||
import BatchKindLoader, { createCoordinate } from "../classes/batch-kind-loader";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
export type RequestOptions = {
|
||||
/** Always request the event from the relays */
|
||||
@ -31,7 +32,7 @@ const WRITE_CACHE_BATCH_TIME = 250;
|
||||
class ReplaceableEventsService {
|
||||
private subjects = new SuperMap<string, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||
private loaders = new SuperMap<string, BatchKindLoader>((relay) => {
|
||||
const loader = new BatchKindLoader(relay, this.log.extend(relay));
|
||||
const loader = new BatchKindLoader(relayPoolService.requestRelay(relay), this.log.extend(relay));
|
||||
loader.events.onEvent.subscribe((e) => this.handleEvent(e));
|
||||
return loader;
|
||||
});
|
||||
|
@ -1,12 +1,12 @@
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import NostrRequest from "../classes/nostr-request";
|
||||
import SuperMap from "../classes/super-map";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { localRelay } from "./local-relay";
|
||||
import { relayRequest, safeRelayUrls } from "../helpers/relay";
|
||||
import { logger } from "../helpers/debug";
|
||||
import Subject from "../classes/subject";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
const RELAY_REQUEST_BATCH_TIME = 500;
|
||||
|
||||
@ -64,9 +64,9 @@ class SingleEventService {
|
||||
}
|
||||
|
||||
for (const [relay, ids] of Object.entries(idsFromRelays)) {
|
||||
const request = new NostrRequest([relay]);
|
||||
request.onEvent.subscribe((event) => this.handleEvent(event));
|
||||
request.start({ ids });
|
||||
const sub = relayPoolService
|
||||
.requestRelay(relay)
|
||||
.subscribe([{ ids }], { onevent: (event) => this.handleEvent(event), oneose: () => sub.close() });
|
||||
}
|
||||
this.pending.clear();
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { kinds, nip18, nip25 } from "nostr-tools";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { NostrEvent, isATag, isETag } from "../../../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||
import { parseZapEvent } from "../../../helpers/nostr/zaps";
|
||||
import { getParsedZap } from "../../../helpers/nostr/zaps";
|
||||
import { readablizeSats } from "../../../helpers/bolt11";
|
||||
import { getEventUID, parseCoordinate } from "../../../helpers/nostr/event";
|
||||
import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event";
|
||||
@ -93,11 +93,7 @@ const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>((
|
||||
});
|
||||
|
||||
const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
||||
const zap = useMemo(() => {
|
||||
try {
|
||||
return parseZapEvent(event);
|
||||
} catch (e) {}
|
||||
}, [event]);
|
||||
const zap = useMemo(() => getParsedZap(event), [event]);
|
||||
|
||||
if (!zap || !zap.payment.amount) return null;
|
||||
|
||||
|
@ -12,21 +12,9 @@ import UploadCloud01 from "../../../components/icons/upload-cloud-01";
|
||||
|
||||
export default function RelayControl({ url }: { url: string }) {
|
||||
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
|
||||
const status = useSubject(relay.status);
|
||||
const writeRelays = useSubject(clientRelaysService.writeRelays);
|
||||
|
||||
let color = "gray";
|
||||
switch (status) {
|
||||
case WebSocket.OPEN:
|
||||
color = "green";
|
||||
break;
|
||||
case WebSocket.CONNECTING:
|
||||
color = "yellow";
|
||||
break;
|
||||
case WebSocket.CLOSED:
|
||||
color = "red";
|
||||
break;
|
||||
}
|
||||
const color = relay.connected ? "green" : "red";
|
||||
|
||||
const onChange = () => {
|
||||
if (writeRelays.has(url)) clientRelaysService.removeRelay(url, RelayMode.WRITE);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { Flex, Heading, IconButton, Link, Text } from "@chakra-ui/react";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
@ -8,7 +9,6 @@ import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { InboxIcon, OutboxIcon } from "../../../components/icons";
|
||||
import MediaServerFavicon from "../../../components/media-server/media-server-favicon";
|
||||
import { RelayMode } from "../../../classes/relay";
|
||||
import { useCallback } from "react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
|
@ -37,9 +37,9 @@ import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { groupByTime } from "../../../helpers/notification";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import EventStore from "../../../classes/event-store";
|
||||
import NostrRequest from "../../../classes/nostr-request";
|
||||
import { sortByDate } from "../../../helpers/nostr/event";
|
||||
import { NostrQuery } from "../../../types/nostr-relay";
|
||||
import relayPoolService from "../../../services/relay-pool";
|
||||
|
||||
ChartJS.register(
|
||||
ArcElement,
|
||||
@ -154,18 +154,21 @@ export default function RelayDetailsTab({ relay }: { relay: string }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const loadMore = useCallback(() => {
|
||||
setLoading(true);
|
||||
const request = new NostrRequest([relay]);
|
||||
const throttle = _throttle(() => update({}), 100);
|
||||
request.onEvent.subscribe((e) => {
|
||||
store.addEvent(e);
|
||||
throttle();
|
||||
});
|
||||
request.onComplete.then(() => setLoading(false));
|
||||
|
||||
const query: NostrQuery = { limit: 500 };
|
||||
const last = store.getLastEvent();
|
||||
if (last) query.until = last.created_at;
|
||||
request.start(query);
|
||||
|
||||
const throttleUpdate = _throttle(() => update({}), 100);
|
||||
const sub = relayPoolService.requestRelay(relay).subscribe([query], {
|
||||
onevent: (event) => {
|
||||
store.addEvent(event);
|
||||
throttleUpdate();
|
||||
},
|
||||
oneose: () => sub.close(),
|
||||
onclose: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
});
|
||||
}, [relay, update, store]);
|
||||
|
||||
useEffect(() => loadMore(), [relay, loadMore]);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { Flex, FlexProps, Text } from "@chakra-ui/react";
|
||||
|
||||
import { parseZapEvent } from "../../../helpers/nostr/zaps";
|
||||
import { parseZapEvents } from "../../../helpers/nostr/zaps";
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
import { LightningIcon } from "../../../components/icons";
|
||||
import { readablizeSats } from "../../../helpers/bolt11";
|
||||
@ -13,15 +13,7 @@ import UserAvatarLink from "../../../components/user/user-avatar-link";
|
||||
export default function TopZappers({ stream, ...props }: FlexProps & { stream: ParsedStream }) {
|
||||
const timeline = useStreamChatTimeline(stream);
|
||||
const events = useSubject(timeline.timeline);
|
||||
const zaps = useMemo(() => {
|
||||
const parsed = [];
|
||||
for (const event of events) {
|
||||
try {
|
||||
parsed.push(parseZapEvent(event));
|
||||
} catch (e) {}
|
||||
}
|
||||
return parsed;
|
||||
}, [events]);
|
||||
const zaps = useMemo(() => parseZapEvents(events), [events]);
|
||||
|
||||
const totals: Record<string, number> = {};
|
||||
for (const zap of zaps) {
|
||||
|
@ -7,7 +7,7 @@ import UserLink from "../../../../components/user/user-link";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../../../../providers/local/intersection-observer";
|
||||
import { LightningIcon } from "../../../../components/icons";
|
||||
import { parseZapEvent } from "../../../../helpers/nostr/zaps";
|
||||
import { getParsedZap } from "../../../../helpers/nostr/zaps";
|
||||
import { readablizeSats } from "../../../../helpers/bolt11";
|
||||
import { TrustProvider } from "../../../../providers/local/trust";
|
||||
import ChatMessageContent from "./chat-message-content";
|
||||
@ -17,11 +17,7 @@ function ZapMessage({ zap, stream }: { zap: NostrEvent; stream: ParsedStream })
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, zap.id);
|
||||
|
||||
const parsed = useMemo(() => {
|
||||
try {
|
||||
return parseZapEvent(zap);
|
||||
} catch (e) {}
|
||||
}, [zap]);
|
||||
const parsed = useMemo(() => getParsedZap(zap), [zap]);
|
||||
const clientMuteFilter = useClientSideMuteFilter();
|
||||
|
||||
if (!parsed || !parsed.payment.amount) return null;
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Box, Button, Flex, Input, Text } from "@chakra-ui/react";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import ForceGraph, { LinkObject, NodeObject } from "react-force-graph-3d";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { Filter, kinds } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDebounce, useObservable } from "react-use";
|
||||
@ -24,10 +24,10 @@ import { getPubkeysFromList } from "../../helpers/nostr/lists";
|
||||
import useUserContactList from "../../hooks/use-user-contact-list";
|
||||
import useUserMetadata from "../../hooks/use-user-metadata";
|
||||
import EventStore from "../../classes/event-store";
|
||||
import NostrRequest from "../../classes/nostr-request";
|
||||
import { isPTag } from "../../types/nostr-event";
|
||||
import { ChevronLeftIcon } from "../../components/icons";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import { subscribeMany } from "../../helpers/relay";
|
||||
|
||||
type NodeType = { id: string; image?: string; name?: string };
|
||||
|
||||
@ -51,13 +51,15 @@ function NetworkDMGraphPage() {
|
||||
if (!contacts) return;
|
||||
|
||||
store.clear();
|
||||
const request = new NostrRequest(relays);
|
||||
request.onEvent.subscribe((e) => store.addEvent(e));
|
||||
request.start({
|
||||
const filter: Filter = {
|
||||
authors: contactsPubkeys,
|
||||
kinds: [kinds.EncryptedDirectMessage],
|
||||
since,
|
||||
until,
|
||||
};
|
||||
const sub = subscribeMany(Array.from(relays), [filter], {
|
||||
onevent: (event) => store.addEvent(event),
|
||||
oneose: () => sub.close(),
|
||||
});
|
||||
},
|
||||
2 * 1000,
|
||||
|
@ -3,15 +3,14 @@ import dayjs from "dayjs";
|
||||
import { ReactNode, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
|
||||
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import { LightningIcon } from "../../components/icons";
|
||||
import { NoteLink } from "../../components/note/note-link";
|
||||
import UserAvatarLink from "../../components/user/user-avatar-link";
|
||||
import UserLink from "../../components/user/user-link";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/nostr/zaps";
|
||||
import { isProfileZap, isNoteZap, totalZaps, parseZapEvents, getParsedZap } from "../../helpers/nostr/zaps";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent, isATag, isETag, isPTag } from "../../types/nostr-event";
|
||||
import { NostrEvent, isATag, isETag } from "../../types/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
@ -23,7 +22,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import { EmbedEventNostrLink, EmbedEventPointer } from "../../components/embed-event";
|
||||
import { EmbedEventPointer } from "../../components/embed-event";
|
||||
import { parseCoordinate } from "../../helpers/nostr/event";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
|
||||
@ -31,7 +30,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, zapEvent.id);
|
||||
|
||||
const { request, payment } = parseZapEvent(zapEvent);
|
||||
const { request, payment } = getParsedZap(zapEvent, false);
|
||||
|
||||
const eventId = request.tags.find(isETag)?.[1];
|
||||
const coordinate = request.tags.find(isATag)?.[1];
|
||||
@ -101,15 +100,7 @@ const UserZapsTab = () => {
|
||||
const timeline = useTimelineLoader(`${pubkey}-zaps`, relays, { "#p": [pubkey], kinds: [9735] }, { eventFilter });
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
const zaps = useMemo(() => {
|
||||
const parsed = [];
|
||||
for (const zap of events) {
|
||||
try {
|
||||
parsed.push(parseZapEvent(zap));
|
||||
} catch (e) {}
|
||||
}
|
||||
return parsed;
|
||||
}, [events]);
|
||||
const zaps = useMemo(() => parseZapEvents(events), [events]);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
|
14
yarn.lock
14
yarn.lock
@ -5435,6 +5435,20 @@ nostr-tools@^2.3.2:
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-tools@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.4.0.tgz#bc2140a95ce0be8d4900bd852f652d811562753e"
|
||||
integrity sha512-xQC7XdGeh0gLyprcKhvx5lwr7OQ+ZOiQ9C6GpzlVAj+EBv+AiN8kySb57t3uJoG1HK15oT9jf++MmQLwhp1xNQ==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.5.1"
|
||||
"@noble/curves" "1.2.0"
|
||||
"@noble/hashes" "1.3.1"
|
||||
"@scure/base" "1.1.1"
|
||||
"@scure/bip32" "1.3.1"
|
||||
"@scure/bip39" "1.2.1"
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-wasm@v0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
|
||||
|
Loading…
x
Reference in New Issue
Block a user