mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-03 16:09:52 +02:00
Merge branch 'next'
This commit is contained in:
5
.changeset/clever-swans-walk.md
Normal file
5
.changeset/clever-swans-walk.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Rebuild observable class
|
5
.changeset/famous-tables-repair.md
Normal file
5
.changeset/famous-tables-repair.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add "open in" modal (NIP-89)
|
5
.changeset/fresh-books-dream.md
Normal file
5
.changeset/fresh-books-dream.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add event publisher tool
|
5
.changeset/heavy-carrots-melt.md
Normal file
5
.changeset/heavy-carrots-melt.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Add UI tab to relays
|
5
.changeset/large-items-knock.md
Normal file
5
.changeset/large-items-knock.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix custom emoji reactions having multiple colons
|
5
.changeset/lovely-dryers-protect.md
Normal file
5
.changeset/lovely-dryers-protect.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix jsonl database export format
|
5
.changeset/nine-chicken-fetch.md
Normal file
5
.changeset/nine-chicken-fetch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Dont auto-play blured videos
|
5
.changeset/popular-pandas-wave.md
Normal file
5
.changeset/popular-pandas-wave.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Added Event Console tool
|
5
.changeset/quick-peas-remain.md
Normal file
5
.changeset/quick-peas-remain.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix bunker://pubkey connect URIs
|
5
.changeset/shiny-ghosts-hear.md
Normal file
5
.changeset/shiny-ghosts-hear.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to automatically decrypt DMs
|
5
.changeset/soft-trees-check.md
Normal file
5
.changeset/soft-trees-check.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix profile form removing unknown metadata fields
|
5
.changeset/strong-camels-whisper.md
Normal file
5
.changeset/strong-camels-whisper.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Unblur all images when clicking on a note
|
5
.changeset/violet-adults-share.md
Normal file
5
.changeset/violet-adults-share.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Update emojilib
|
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 120
|
||||
}
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -13,5 +13,6 @@
|
||||
"webln"
|
||||
],
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"deno.enable": false
|
||||
}
|
||||
|
12
package.json
12
package.json
@@ -24,6 +24,9 @@
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@chakra-ui/shared-utils": "^2.0.4",
|
||||
"@chakra-ui/styled-system": "^2.9.2",
|
||||
"@codemirror/autocomplete": "^6.12.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@getalby/bitcoin-connect": "^3.2.1",
|
||||
@@ -31,15 +34,18 @@
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"@noble/secp256k1": "^1.7.0",
|
||||
"@uiw/codemirror-theme-github": "^4.21.21",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||
"bech32": "^2.0.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"chart.js": "^4.4.1",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"chroma-js": "^2.4.2",
|
||||
"codemirror-json-schema": "^0.6.1",
|
||||
"dayjs": "^1.11.9",
|
||||
"debug": "^4.3.4",
|
||||
"emojilib": "2",
|
||||
"emojilib": "^3",
|
||||
"framer-motion": "^10.16.0",
|
||||
"hls.js": "^1.4.14",
|
||||
"idb": "^8.0.0",
|
||||
@@ -73,7 +79,8 @@
|
||||
"three-spritetext": "^1.8.1",
|
||||
"three-stdlib": "^2.29.4",
|
||||
"webln": "^0.3.2",
|
||||
"yet-another-react-lightbox": "^3.15.6"
|
||||
"yet-another-react-lightbox": "^3.15.6",
|
||||
"zen-observable": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.1",
|
||||
@@ -89,6 +96,7 @@
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/three": "^0.160.0",
|
||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
|
||||
"@types/zen-observable": "^0.8.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"camelcase": "^8.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
|
@@ -100,6 +100,8 @@ const StreamModerationView = lazy(() => import("./views/streams/dashboard"));
|
||||
const NetworkMuteGraphView = lazy(() => import("./views/tools/network-mute-graph"));
|
||||
const NetworkDMGraphView = lazy(() => import("./views/tools/network-dm-graph"));
|
||||
const UnknownTimelineView = lazy(() => import("./views/tools/unknown-event-feed"));
|
||||
const EventConsoleView = lazy(() => import("./views/tools/event-console"));
|
||||
const EventPublisherView = lazy(() => import("./views/tools/event-publisher"));
|
||||
|
||||
const UserStreamsTab = lazy(() => import("./views/user/streams"));
|
||||
const StreamsView = lazy(() => import("./views/streams"));
|
||||
@@ -321,6 +323,8 @@ const router = createHashRouter([
|
||||
{ path: "transform/:id", element: <TransformNoteView /> },
|
||||
{ path: "satellite-cdn", element: <SatelliteCDNView /> },
|
||||
{ path: "unknown", element: <UnknownTimelineView /> },
|
||||
{ path: "console", element: <EventConsoleView /> },
|
||||
{ path: "publisher", element: <EventPublisherView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
118
src/classes/batch-kind-loader.ts
Normal file
118
src/classes/batch-kind-loader.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Filter, NostrEvent } 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";
|
||||
|
||||
export function createCoordinate(kind: number, pubkey: string, d?: string) {
|
||||
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
|
||||
}
|
||||
|
||||
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;
|
||||
events = new EventStore();
|
||||
|
||||
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));
|
||||
|
||||
this.log = log || debug("RelayBatchLoader");
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
const key = getEventCoordinate(event);
|
||||
|
||||
// remove the key from the waiting list
|
||||
this.requested.delete(key);
|
||||
|
||||
const current = this.events.getEvent(key);
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
this.events.addEvent(event);
|
||||
}
|
||||
}
|
||||
private handleEOSE() {
|
||||
// relays says it has nothing left
|
||||
this.requested.clear();
|
||||
}
|
||||
|
||||
requestEvent(kind: number, pubkey: string, d?: string) {
|
||||
const key = createCoordinate(kind, pubkey, d);
|
||||
const event = this.events.getEvent(key);
|
||||
if (!event) {
|
||||
this.requestNext.add(key);
|
||||
this.updateThrottle();
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME);
|
||||
update() {
|
||||
let needsUpdate = false;
|
||||
for (const key of this.requestNext) {
|
||||
if (!this.requested.has(key)) {
|
||||
this.requested.set(key, new Date());
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
this.requestNext.clear();
|
||||
|
||||
// prune requests
|
||||
const timeout = dayjs().subtract(1, "minute");
|
||||
for (const [key, date] of this.requested) {
|
||||
if (dayjs(date).isBefore(timeout)) {
|
||||
this.requested.delete(key);
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// update the subscription
|
||||
if (needsUpdate) {
|
||||
if (this.requested.size > 0) {
|
||||
const filters: Record<number, Filter> = {};
|
||||
|
||||
for (const [cord] of this.requested) {
|
||||
const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string];
|
||||
const kind = parseInt(kindStr);
|
||||
filters[kind] = filters[kind] || { kinds: [kind] };
|
||||
|
||||
const arr = (filters[kind].authors = filters[kind].authors || []);
|
||||
arr.push(pubkey);
|
||||
|
||||
if (d) {
|
||||
const arr = (filters[kind]["#d"] = filters[kind]["#d"] || []);
|
||||
arr.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
const query = Array.from(Object.values(filters));
|
||||
|
||||
this.log(
|
||||
`Updating query`,
|
||||
Array.from(Object.keys(filters))
|
||||
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
|
||||
.join(", "),
|
||||
);
|
||||
this.subscription.setFilters(query);
|
||||
|
||||
if (this.subscription.state !== NostrSubscription.OPEN) {
|
||||
this.subscription.open();
|
||||
}
|
||||
} else if (this.subscription.state === NostrSubscription.OPEN) {
|
||||
this.subscription.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
src/classes/chunked-request.ts
Normal file
93
src/classes/chunked-request.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Debugger } from "debug";
|
||||
import { Filter, NostrEvent, 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";
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 100;
|
||||
|
||||
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
|
||||
|
||||
export default class ChunkedRequest {
|
||||
relay: string;
|
||||
filters: Filter[];
|
||||
chunkSize = DEFAULT_CHUNK_SIZE;
|
||||
private log: Debugger;
|
||||
private subs: ZenObservable.Subscription[] = [];
|
||||
|
||||
loading = false;
|
||||
events: EventStore;
|
||||
/** set to true when the next chunk produces 0 events */
|
||||
complete = false;
|
||||
|
||||
onChunkFinish = new Subject<number>();
|
||||
|
||||
constructor(relay: string, filters: Filter[], log?: Debugger) {
|
||||
this.relay = relay;
|
||||
this.filters = filters;
|
||||
|
||||
this.log = log || logger.extend(relay);
|
||||
this.events = new EventStore(relay);
|
||||
|
||||
// TODO: find a better place for this
|
||||
this.subs.push(deleteEventService.stream.subscribe((e) => this.handleDeleteEvent(e)));
|
||||
}
|
||||
|
||||
loadNextChunk() {
|
||||
this.loading = true;
|
||||
let filters: Filter[] = mergeFilter(this.filters, { limit: this.chunkSize });
|
||||
let oldestEvent = this.getLastEvent();
|
||||
if (oldestEvent) {
|
||||
filters = mergeFilter(filters, { until: oldestEvent.created_at - 1 });
|
||||
}
|
||||
|
||||
const request = new NostrRequest([this.relay]);
|
||||
|
||||
let gotEvents = 0;
|
||||
request.onEvent.subscribe((e) => {
|
||||
this.handleEvent(e);
|
||||
gotEvents++;
|
||||
});
|
||||
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) {
|
||||
if (!matchFilters(this.filters, event)) return;
|
||||
return this.events.addEvent(event);
|
||||
}
|
||||
|
||||
private handleDeleteEvent(deleteEvent: NostrEvent) {
|
||||
const cord = deleteEvent.tags.find(isATag)?.[1];
|
||||
const eventId = deleteEvent.tags.find(isETag)?.[1];
|
||||
|
||||
if (cord) this.events.deleteEvent(cord);
|
||||
if (eventId) this.events.deleteEvent(eventId);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const sub of this.subs) sub.unsubscribe();
|
||||
this.subs = [];
|
||||
}
|
||||
|
||||
getFirstEvent(nth = 0, eventFilter?: EventFilter) {
|
||||
return this.events.getFirstEvent(nth, eventFilter);
|
||||
}
|
||||
getLastEvent(nth = 0, eventFilter?: EventFilter) {
|
||||
return this.events.getLastEvent(nth, eventFilter);
|
||||
}
|
||||
}
|
64
src/classes/controlled-observable.ts
Normal file
64
src/classes/controlled-observable.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import Observable from "zen-observable";
|
||||
|
||||
export default class ControlledObservable<T> implements Observable<T> {
|
||||
private observable: Observable<T>;
|
||||
private subscriptions = new Set<ZenObservable.SubscriptionObserver<T>>();
|
||||
private _complete = false;
|
||||
get closed() {
|
||||
return this._complete;
|
||||
}
|
||||
get used() {
|
||||
return this.subscriptions.size > 0;
|
||||
}
|
||||
|
||||
constructor(subscriber?: ZenObservable.Subscriber<T>) {
|
||||
this.observable = new Observable((observer) => {
|
||||
this.subscriptions.add(observer);
|
||||
const cleanup = subscriber && subscriber(observer);
|
||||
return () => {
|
||||
this.subscriptions.delete(observer);
|
||||
if (typeof cleanup === "function") cleanup();
|
||||
else if (cleanup?.unsubscribe) cleanup.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
this.subscribe = this.observable.subscribe.bind(this.observable);
|
||||
this.map = this.observable.map.bind(this.observable);
|
||||
this.flatMap = this.observable.flatMap.bind(this.observable);
|
||||
this.forEach = this.observable.forEach.bind(this.observable);
|
||||
this.reduce = this.observable.reduce.bind(this.observable);
|
||||
this.filter = this.observable.filter.bind(this.observable);
|
||||
this.concat = this.observable.concat.bind(this.observable);
|
||||
}
|
||||
|
||||
next(v: T) {
|
||||
if (this._complete) return;
|
||||
for (const observer of this.subscriptions) {
|
||||
observer.next(v);
|
||||
}
|
||||
}
|
||||
error(err: any) {
|
||||
if (this._complete) return;
|
||||
for (const observer of this.subscriptions) {
|
||||
observer.error(err);
|
||||
}
|
||||
}
|
||||
complete() {
|
||||
if (this._complete) return;
|
||||
this._complete = true;
|
||||
for (const observer of this.subscriptions) {
|
||||
observer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.observable]() {
|
||||
return this.observable;
|
||||
}
|
||||
subscribe: Observable<T>["subscribe"];
|
||||
map: Observable<T>["map"];
|
||||
flatMap: Observable<T>["flatMap"];
|
||||
forEach: Observable<T>["forEach"];
|
||||
reduce: Observable<T>["reduce"];
|
||||
filter: Observable<T>["filter"];
|
||||
concat: Observable<T>["concat"];
|
||||
}
|
@@ -1,52 +1,53 @@
|
||||
import { getEventUID, isReplaceable, sortByDate } from "../helpers/nostr/events";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import { NostrEvent, isDTag } from "../types/nostr-event";
|
||||
import Subject from "./subject";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { getEventUID, sortByDate } from "../helpers/nostr/event";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import SuperMap from "./super-map";
|
||||
import deleteEventService from "../services/delete-events";
|
||||
|
||||
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
|
||||
|
||||
/** a class used to store and sort events */
|
||||
export default class EventStore {
|
||||
id = nanoid(8);
|
||||
name?: string;
|
||||
events = new Map<string, NostrEvent>();
|
||||
|
||||
customSort?: typeof sortByDate;
|
||||
|
||||
private deleteSub: ZenObservable.Subscription;
|
||||
|
||||
constructor(name?: string, customSort?: typeof sortByDate) {
|
||||
this.name = name;
|
||||
this.customSort = customSort;
|
||||
|
||||
this.deleteSub = deleteEventService.stream.subscribe((event) => {
|
||||
const uid = getEventUID(event);
|
||||
this.deleteEvent(uid);
|
||||
if (uid !== event.id) this.deleteEvent(event.id);
|
||||
});
|
||||
}
|
||||
|
||||
getSortedEvents() {
|
||||
return Array.from(this.events.values()).sort(this.customSort || sortByDate);
|
||||
}
|
||||
|
||||
onEvent = new Subject<NostrEvent>(undefined, false);
|
||||
onDelete = new Subject<string>(undefined, false);
|
||||
onClear = new Subject(undefined, false);
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
onDelete = new ControlledObservable<string>();
|
||||
onClear = new ControlledObservable();
|
||||
|
||||
private replaceableEventSubs = new Map<string, Subject<NostrEvent>>();
|
||||
private handleEvent(event: NostrEvent) {
|
||||
const id = getEventUID(event);
|
||||
const existing = this.events.get(id);
|
||||
const uid = getEventUID(event);
|
||||
const existing = this.events.get(uid);
|
||||
if (!existing || event.created_at > existing.created_at) {
|
||||
this.events.set(id, event);
|
||||
this.events.set(uid, event);
|
||||
this.onEvent.next(event);
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(event: NostrEvent) {
|
||||
const id = getEventUID(event);
|
||||
this.handleEvent(event);
|
||||
|
||||
if (isReplaceable(event.kind)) {
|
||||
// pass the event on
|
||||
replaceableEventLoaderService.handleEvent(event);
|
||||
|
||||
// subscribe to any future changes
|
||||
const sub = replaceableEventLoaderService.getEvent(event.kind, event.pubkey, event.tags.find(isDTag)?.[1]);
|
||||
sub.subscribe(this.handleEvent, this);
|
||||
this.replaceableEventSubs.set(id, sub);
|
||||
}
|
||||
}
|
||||
getEvent(id: string) {
|
||||
return this.events.get(id);
|
||||
@@ -56,32 +57,36 @@ export default class EventStore {
|
||||
this.events.delete(id);
|
||||
this.onDelete.next(id);
|
||||
}
|
||||
|
||||
if (this.replaceableEventSubs.has(id)) {
|
||||
this.replaceableEventSubs.get(id)?.unsubscribe(this.handleEvent, this);
|
||||
this.replaceableEventSubs.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.events.clear();
|
||||
this.onClear.next(undefined);
|
||||
|
||||
for (const [_, sub] of this.replaceableEventSubs) {
|
||||
sub.unsubscribe(this.handleEvent, this);
|
||||
}
|
||||
}
|
||||
cleanup() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
connect(other: EventStore) {
|
||||
other.onEvent.subscribe(this.addEvent, this);
|
||||
other.onDelete.subscribe(this.deleteEvent, this);
|
||||
private storeSubs = new SuperMap<EventStore, ZenObservable.Subscription[]>(() => []);
|
||||
connect(other: EventStore, fullSync = true) {
|
||||
const subs = this.storeSubs.get(other);
|
||||
subs.push(
|
||||
other.onEvent.subscribe((e) => {
|
||||
if (fullSync || this.events.has(getEventUID(e))) this.addEvent(e);
|
||||
}),
|
||||
);
|
||||
subs.push(other.onDelete.subscribe(this.deleteEvent.bind(this)));
|
||||
}
|
||||
disconnect(other: EventStore) {
|
||||
other.onEvent.unsubscribe(this.addEvent, this);
|
||||
other.onDelete.unsubscribe(this.deleteEvent, this);
|
||||
const subs = this.storeSubs.get(other);
|
||||
for (const sub of subs) sub.unsubscribe();
|
||||
this.storeSubs.delete(other);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.clear();
|
||||
for (const [_, subs] of this.storeSubs) {
|
||||
for (const sub of subs) sub.unsubscribe();
|
||||
}
|
||||
this.storeSubs.clear();
|
||||
this.deleteSub.unsubscribe();
|
||||
}
|
||||
|
||||
getFirstEvent(nth = 0, filter?: EventFilter) {
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { Subject } from "./subject";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingRequest, NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
|
||||
import Relay, { IncomingEvent } from "./relay";
|
||||
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-relay";
|
||||
import Relay, { IncomingEvent, OutgoingRequest } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { isFilterEqual, isQueryMapEqual } from "../helpers/nostr/filter";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import SuperMap from "./super-map";
|
||||
|
||||
export default class NostrMultiSubscription {
|
||||
static INIT = "initial";
|
||||
@@ -18,36 +19,38 @@ export default class NostrMultiSubscription {
|
||||
|
||||
relays: Relay[] = [];
|
||||
state = NostrMultiSubscription.INIT;
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
seenEvents = new Set<string>();
|
||||
|
||||
constructor(name?: string) {
|
||||
this.id = nanoid();
|
||||
this.name = name;
|
||||
}
|
||||
private handleEvent(incomingEvent: IncomingEvent) {
|
||||
private handleMessage(incomingEvent: IncomingEvent) {
|
||||
if (
|
||||
this.state === NostrMultiSubscription.OPEN &&
|
||||
incomingEvent.subId === this.id &&
|
||||
!this.seenEvents.has(incomingEvent.body.id)
|
||||
incomingEvent[1] === this.id &&
|
||||
!this.seenEvents.has(incomingEvent[2].id)
|
||||
) {
|
||||
this.onEvent.next(incomingEvent.body);
|
||||
this.seenEvents.add(incomingEvent.body.id);
|
||||
this.onEvent.next(incomingEvent[2]);
|
||||
this.seenEvents.add(incomingEvent[2].id);
|
||||
}
|
||||
}
|
||||
|
||||
private relaySubs = new SuperMap<Relay, ZenObservable.Subscription[]>(() => []);
|
||||
/** listen for event and open events from relays */
|
||||
private connectToRelay(relay: Relay) {
|
||||
relay.onEvent.subscribe(this.handleEvent, this);
|
||||
relay.onOpen.subscribe(this.handleRelayConnect, this);
|
||||
relay.onClose.subscribe(this.handleRelayDisconnect, this);
|
||||
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)));
|
||||
relayPoolService.addClaim(relay.url, this);
|
||||
}
|
||||
/** stop listing to events from relays */
|
||||
private disconnectFromRelay(relay: Relay) {
|
||||
relay.onEvent.unsubscribe(this.handleEvent, this);
|
||||
relay.onOpen.unsubscribe(this.handleRelayConnect, this);
|
||||
relay.onClose.unsubscribe(this.handleRelayDisconnect, this);
|
||||
const subs = this.relaySubs.get(relay);
|
||||
for (const sub of subs) sub.unsubscribe();
|
||||
this.relaySubs.delete(relay);
|
||||
relayPoolService.removeClaim(relay.url, this);
|
||||
|
||||
// if the subscription is open and had sent a request to the relay
|
||||
@@ -90,9 +93,7 @@ export default class NostrMultiSubscription {
|
||||
|
||||
for (const relay of this.relays) {
|
||||
const filter = this.queryMap[relay.url];
|
||||
const message: NostrOutgoingRequest = Array.isArray(filter)
|
||||
? ["REQ", this.id, ...filter]
|
||||
: ["REQ", this.id, filter];
|
||||
const message: OutgoingRequest = Array.isArray(filter) ? ["REQ", this.id, ...filter] : ["REQ", this.id, filter];
|
||||
|
||||
const currentFilter = this.relayQueries.get(relay);
|
||||
if (!currentFilter || !isFilterEqual(currentFilter, filter)) {
|
||||
|
@@ -4,7 +4,9 @@ import { NostrEvent } from "nostr-tools";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import createDefer from "./deferred";
|
||||
import Relay, { IncomingCommandResult } from "./relay";
|
||||
import Subject, { PersistentSubject } from "./subject";
|
||||
import { PersistentSubject } from "./subject";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import SuperMap from "./super-map";
|
||||
|
||||
export default class NostrPublishAction {
|
||||
id = nanoid();
|
||||
@@ -12,11 +14,13 @@ export default class NostrPublishAction {
|
||||
relays: string[];
|
||||
event: NostrEvent;
|
||||
|
||||
results = new PersistentSubject<IncomingCommandResult[]>([]);
|
||||
onResult = new Subject<IncomingCommandResult>(undefined, false);
|
||||
onComplete = createDefer<IncomingCommandResult[]>();
|
||||
results = new PersistentSubject<{ relay: Relay; result: IncomingCommandResult }[]>([]);
|
||||
|
||||
onResult = new ControlledObservable<{ relay: Relay; result: IncomingCommandResult }>();
|
||||
onComplete = createDefer<{ relay: Relay; result: IncomingCommandResult }[]>();
|
||||
|
||||
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;
|
||||
@@ -26,37 +30,31 @@ export default class NostrPublishAction {
|
||||
for (const url of relays) {
|
||||
const relay = relayPoolService.requestRelay(url);
|
||||
this.remaining.add(relay);
|
||||
relay.onCommandResult.subscribe(this.handleResult, this);
|
||||
this.relayResultSubs.get(relay).push(
|
||||
relay.onCommandResult.subscribe((result) => {
|
||||
if (result[1] === this.event.id) this.handleResult(result, relay);
|
||||
}),
|
||||
);
|
||||
|
||||
// send event
|
||||
relay.send(["EVENT", event]);
|
||||
}
|
||||
|
||||
setTimeout(this.handleTimeout.bind(this), timeout);
|
||||
}
|
||||
|
||||
private handleResult(result: IncomingCommandResult) {
|
||||
if (result.eventId === this.event.id) {
|
||||
const relay = result.relay;
|
||||
this.results.next([...this.results.value, result]);
|
||||
private handleResult(result: IncomingCommandResult, relay: Relay) {
|
||||
this.results.next([...this.results.value, { relay, result }]);
|
||||
this.onResult.next({ relay, result });
|
||||
|
||||
this.onResult.next(result);
|
||||
|
||||
relay.onCommandResult.unsubscribe(this.handleResult, this);
|
||||
this.remaining.delete(relay);
|
||||
if (this.remaining.size === 0) this.onComplete.resolve(this.results.value);
|
||||
}
|
||||
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({
|
||||
message: "Timeout",
|
||||
eventId: this.event.id,
|
||||
status: false,
|
||||
type: "OK",
|
||||
relay,
|
||||
});
|
||||
this.handleResult(["OK", this.event.id, false, "Timeout"], relay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { CountResponse, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrRequestFilter } from "../types/nostr-query";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import Relay, { IncomingCount, IncomingEOSE, IncomingEvent } from "./relay";
|
||||
import Subject from "./subject";
|
||||
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 {
|
||||
@@ -12,36 +13,37 @@ export default class NostrRequest {
|
||||
static RUNNING = "running";
|
||||
static COMPLETE = "complete";
|
||||
|
||||
id: string;
|
||||
id = nanoid();
|
||||
timeout: number;
|
||||
relays: Set<Relay>;
|
||||
state = NostrRequest.IDLE;
|
||||
onEvent = new Subject<NostrEvent>(undefined, false);
|
||||
onCount = new Subject<CountResponse>(undefined, false);
|
||||
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.id = nanoid();
|
||||
this.relays = new Set(Array.from(relayUrls).map((url) => relayPoolService.requestRelay(url)));
|
||||
|
||||
for (const relay of this.relays) {
|
||||
relay.onEOSE.subscribe(this.handleEOSE, this);
|
||||
relay.onEvent.subscribe(this.handleEvent, this);
|
||||
relay.onCount.subscribe(this.handleCount, this);
|
||||
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(eose: IncomingEOSE) {
|
||||
if (eose.subId === this.id) {
|
||||
const relay = eose.relay;
|
||||
handleEOSE(message: IncomingEOSE, relay: Relay) {
|
||||
if (message[1] === this.id) {
|
||||
this.relays.delete(relay);
|
||||
relay.send(["CLOSE", this.id]);
|
||||
|
||||
relay.onEOSE.unsubscribe(this.handleEOSE, this);
|
||||
relay.onEvent.unsubscribe(this.handleEvent, this);
|
||||
this.relaySubs.get(relay).forEach((sub) => sub.unsubscribe());
|
||||
this.relaySubs.delete(relay);
|
||||
|
||||
if (this.relays.size === 0) {
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
@@ -49,23 +51,19 @@ export default class NostrRequest {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleEvent(incomingEvent: IncomingEvent) {
|
||||
if (
|
||||
this.state === NostrRequest.RUNNING &&
|
||||
incomingEvent.subId === this.id &&
|
||||
!this.seenEvents.has(incomingEvent.body.id)
|
||||
) {
|
||||
this.onEvent.next(incomingEvent.body);
|
||||
this.seenEvents.add(incomingEvent.body.id);
|
||||
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.subId === this.id) {
|
||||
this.onCount.next({ count: incomingCount.count, approximate: incomingCount.approximate });
|
||||
if (incomingCount[1] === this.id) {
|
||||
this.onCount.next(incomingCount[2]);
|
||||
}
|
||||
}
|
||||
|
||||
start(filter: NostrRequestFilter, type: "REQ" | "COUNT" = "REQ") {
|
||||
start(filter: Filter | Filter[], type: "REQ" | "COUNT" = "REQ") {
|
||||
if (this.state !== NostrRequest.IDLE) {
|
||||
throw new Error("cant restart a nostr request");
|
||||
}
|
||||
@@ -87,9 +85,9 @@ export default class NostrRequest {
|
||||
this.state = NostrRequest.COMPLETE;
|
||||
for (const relay of this.relays) {
|
||||
relay.send(["CLOSE", this.id]);
|
||||
relay.onEOSE.unsubscribe(this.handleEOSE, this);
|
||||
relay.onEvent.unsubscribe(this.handleEvent, this);
|
||||
this.relaySubs.get(relay).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
this.relaySubs.clear();
|
||||
this.onComplete.resolve();
|
||||
|
||||
return this;
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage, NostrRequestFilter } from "../types/nostr-query";
|
||||
import Relay, { IncomingEOSE } from "./relay";
|
||||
import Relay, { IncomingEOSE, OutgoingMessage } from "./relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { Subject } from "./subject";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
|
||||
export default class NostrSubscription {
|
||||
static INIT = "initial";
|
||||
@@ -13,55 +12,58 @@ export default class NostrSubscription {
|
||||
|
||||
id: string;
|
||||
name?: string;
|
||||
query?: NostrRequestFilter;
|
||||
filters?: Filter[];
|
||||
relay: Relay;
|
||||
state = NostrSubscription.INIT;
|
||||
onEvent = new Subject<NostrEvent>();
|
||||
onEOSE = new Subject<IncomingEOSE>();
|
||||
|
||||
constructor(relayUrl: string | URL, query?: NostrRequestFilter, name?: string) {
|
||||
onEvent = new ControlledObservable<NostrEvent>();
|
||||
onEOSE = new ControlledObservable<IncomingEOSE>();
|
||||
|
||||
private subs: ZenObservable.Subscription[] = [];
|
||||
|
||||
constructor(relayUrl: string | URL, filters?: Filter[], name?: string) {
|
||||
this.id = nanoid();
|
||||
this.query = query;
|
||||
this.filters = filters;
|
||||
this.name = name;
|
||||
|
||||
this.relay = relayPoolService.requestRelay(relayUrl);
|
||||
|
||||
this.onEvent.connectWithHandler(this.relay.onEvent, (event, next) => {
|
||||
if (this.state === NostrSubscription.OPEN) {
|
||||
next(event.body);
|
||||
}
|
||||
});
|
||||
this.onEOSE.connectWithHandler(this.relay.onEOSE, (eose, next) => {
|
||||
if (this.state === NostrSubscription.OPEN) next(eose);
|
||||
});
|
||||
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: NostrOutgoingMessage) {
|
||||
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]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
open() {
|
||||
if (!this.query) throw new Error("cant open without a query");
|
||||
if (!this.filters) throw new Error("cant open without a query");
|
||||
if (this.state === NostrSubscription.OPEN) return this;
|
||||
|
||||
this.state = NostrSubscription.OPEN;
|
||||
if (Array.isArray(this.query)) {
|
||||
this.send(["REQ", this.id, ...this.query]);
|
||||
} else this.send(["REQ", this.id, this.query]);
|
||||
this.send(["REQ", this.id, ...this.filters]);
|
||||
|
||||
relayPoolService.addClaim(this.relay.url, this);
|
||||
|
||||
return this;
|
||||
}
|
||||
setQuery(query: NostrRequestFilter) {
|
||||
this.query = query;
|
||||
if (this.state === NostrSubscription.OPEN) {
|
||||
if (Array.isArray(this.query)) {
|
||||
this.send(["REQ", this.id, ...this.query]);
|
||||
} else this.send(["REQ", this.id, this.query]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
close() {
|
||||
if (this.state !== NostrSubscription.OPEN) return this;
|
||||
|
||||
@@ -72,6 +74,9 @@ export default class NostrSubscription {
|
||||
// unsubscribe from relay messages
|
||||
relayPoolService.removeClaim(this.relay.url, this);
|
||||
|
||||
for (const sub of this.subs) sub.unsubscribe();
|
||||
this.subs = [];
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
90
src/classes/relay-pool.ts
Normal file
90
src/classes/relay-pool.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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 {
|
||||
relays = new Map<string, Relay>();
|
||||
onRelayCreated = new Subject<Relay>();
|
||||
|
||||
relayClaims = new Map<string, Set<any>>();
|
||||
|
||||
log = logger.extend("RelayPool");
|
||||
|
||||
getRelays() {
|
||||
return Array.from(this.relays.values());
|
||||
}
|
||||
getRelayClaims(url: string | URL) {
|
||||
url = validateRelayURL(url);
|
||||
const key = url.toString();
|
||||
if (!this.relayClaims.has(key)) {
|
||||
this.relayClaims.set(key, new Set());
|
||||
}
|
||||
return this.relayClaims.get(key) as Set<any>;
|
||||
}
|
||||
|
||||
requestRelay(url: string | URL, connect = true) {
|
||||
url = validateRelayURL(url);
|
||||
const key = url.toString();
|
||||
if (!this.relays.has(key)) {
|
||||
const newRelay = new Relay(key);
|
||||
this.relays.set(key, newRelay);
|
||||
this.onRelayCreated.next(newRelay);
|
||||
}
|
||||
|
||||
const relay = this.relays.get(key) as Relay;
|
||||
if (connect && !relay.okay) {
|
||||
try {
|
||||
relay.open();
|
||||
} catch (e) {
|
||||
this.log(`Failed to connect to ${relay.url}`);
|
||||
this.log(e);
|
||||
}
|
||||
}
|
||||
return relay;
|
||||
}
|
||||
|
||||
pruneRelays() {
|
||||
for (const [url, relay] of this.relays.entries()) {
|
||||
const claims = this.getRelayClaims(url).size;
|
||||
if (claims === 0) {
|
||||
relay.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
reconnectRelays() {
|
||||
if (offlineMode.value) return;
|
||||
|
||||
for (const [url, relay] of this.relays.entries()) {
|
||||
const claims = this.getRelayClaims(url).size;
|
||||
if (!relay.okay && claims > 0) {
|
||||
try {
|
||||
relay.open();
|
||||
} catch (e) {
|
||||
this.log(`Failed to connect to ${relay.url}`);
|
||||
this.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addClaim(url: string | URL, id: any) {
|
||||
url = validateRelayURL(url);
|
||||
const key = url.toString();
|
||||
this.getRelayClaims(key).add(id);
|
||||
}
|
||||
removeClaim(url: string | URL, id: any) {
|
||||
url = validateRelayURL(url);
|
||||
const key = url.toString();
|
||||
this.getRelayClaims(key).delete(id);
|
||||
}
|
||||
|
||||
get connectedCount() {
|
||||
let count = 0;
|
||||
for (const [url, relay] of this.relays.entries()) {
|
||||
if (relay.connected) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
@@ -1,9 +1,8 @@
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { relaysFromContactsEvent } from "../helpers/nostr/contacts";
|
||||
import { getRelaysFromMailbox } from "../helpers/nostr/mailbox";
|
||||
import { safeJson } from "../helpers/parse";
|
||||
import { safeRelayUrl } from "../helpers/relay";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { RelayMode } from "./relay";
|
||||
|
||||
export default class RelaySet extends Set<string> {
|
||||
|
@@ -1,39 +1,28 @@
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { offlineMode } from "../services/offline-mode";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
import { RawIncomingNostrEvent, NostrEvent, CountResponse } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage } from "../types/nostr-query";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import createDefer, { Deferred } from "./deferred";
|
||||
import { PersistentSubject, Subject } from "./subject";
|
||||
import { PersistentSubject } from "./subject";
|
||||
|
||||
export type IncomingEvent = {
|
||||
type: "EVENT";
|
||||
subId: string;
|
||||
body: NostrEvent;
|
||||
relay: Relay;
|
||||
};
|
||||
export type IncomingNotice = {
|
||||
type: "NOTICE";
|
||||
message: string;
|
||||
relay: Relay;
|
||||
};
|
||||
export type IncomingCount = {
|
||||
type: "COUNT";
|
||||
subId: string;
|
||||
relay: Relay;
|
||||
} & CountResponse;
|
||||
export type IncomingEOSE = {
|
||||
type: "EOSE";
|
||||
subId: string;
|
||||
relay: Relay;
|
||||
};
|
||||
export type IncomingCommandResult = {
|
||||
type: "OK";
|
||||
eventId: string;
|
||||
status: boolean;
|
||||
message?: string;
|
||||
relay: Relay;
|
||||
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,
|
||||
@@ -45,15 +34,16 @@ const CONNECTION_TIMEOUT = 1000 * 30;
|
||||
|
||||
export default class Relay {
|
||||
url: string;
|
||||
status = new PersistentSubject<number>(WebSocket.CLOSED);
|
||||
onOpen = new Subject<Relay>(undefined, false);
|
||||
onClose = new Subject<Relay>(undefined, false);
|
||||
onEvent = new Subject<IncomingEvent>(undefined, false);
|
||||
onNotice = new Subject<IncomingNotice>(undefined, false);
|
||||
onCount = new Subject<IncomingCount>(undefined, false);
|
||||
onEOSE = new Subject<IncomingEOSE>(undefined, false);
|
||||
onCommandResult = new Subject<IncomingCommandResult>(undefined, false);
|
||||
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>[] = [];
|
||||
|
||||
@@ -61,7 +51,7 @@ export default class Relay {
|
||||
private ejectTimer?: () => void;
|
||||
private intentionalClose = false;
|
||||
private subscriptionResTimer = new Map<string, () => void>();
|
||||
private queue: NostrOutgoingMessage[] = [];
|
||||
private queue: OutgoingMessage[] = [];
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
@@ -122,7 +112,7 @@ export default class Relay {
|
||||
};
|
||||
this.ws.onmessage = this.handleMessage.bind(this);
|
||||
}
|
||||
send(json: NostrOutgoingMessage) {
|
||||
send(json: OutgoingMessage) {
|
||||
if (this.connected) {
|
||||
this.ws?.send(JSON.stringify(json));
|
||||
|
||||
@@ -184,35 +174,38 @@ export default class Relay {
|
||||
return this.ws?.readyState;
|
||||
}
|
||||
|
||||
handleMessage(event: MessageEvent<string>) {
|
||||
if (!event.data) return;
|
||||
handleMessage(message: MessageEvent<string>) {
|
||||
if (!message.data) return;
|
||||
|
||||
try {
|
||||
const data: RawIncomingNostrEvent = JSON.parse(event.data);
|
||||
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({ relay: this, type, subId: data[1], body: data[2] });
|
||||
this.onEvent.next(data);
|
||||
this.endSubResTimer(data[1]);
|
||||
break;
|
||||
case "NOTICE":
|
||||
this.onNotice.next({ relay: this, type, message: data[1] });
|
||||
this.onNotice.next(data);
|
||||
break;
|
||||
case "COUNT":
|
||||
this.onCount.next({ relay: this, type, subId: data[1], ...data[2] });
|
||||
this.onCount.next(data);
|
||||
break;
|
||||
case "EOSE":
|
||||
this.onEOSE.next({ relay: this, type, subId: data[1] });
|
||||
this.onEOSE.next(data);
|
||||
this.endSubResTimer(data[1]);
|
||||
break;
|
||||
case "OK":
|
||||
this.onCommandResult.next({ relay: this, type, eventId: data[1], status: data[2], message: data[3] });
|
||||
this.onCommandResult.next(data);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Relay: Failed to parse event from ${this.url}`);
|
||||
console.log(event.data, e);
|
||||
console.log(`Relay: Failed to parse massage from ${this.url}`);
|
||||
console.log(message.data, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,111 +1,62 @@
|
||||
export type ListenerFn<T> = (value: T) => void;
|
||||
interface Connectable<Value> {
|
||||
value?: Value;
|
||||
subscribe(listener: ListenerFn<Value>, ctx?: Object): this;
|
||||
unsubscribe(listener: ListenerFn<Value>, ctx?: Object): this;
|
||||
}
|
||||
interface ConnectableApi<T> {
|
||||
connect(connectable: Connectable<T>): this;
|
||||
disconnect(connectable: Connectable<T>): this;
|
||||
}
|
||||
export type Connection<From, To = From, Prev = To> = (value: From, next: (value: To) => any, prevValue: Prev) => void;
|
||||
import Observable from "zen-observable";
|
||||
import { nanoid } from "nanoid";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
|
||||
export class Subject<Value> implements Connectable<Value> {
|
||||
listeners: [ListenerFn<Value>, Object | undefined][] = [];
|
||||
/** An observable that is always open and stores the last value */
|
||||
export default class Subject<T> {
|
||||
private observable: ControlledObservable<T>;
|
||||
id = nanoid(8);
|
||||
value: T | undefined;
|
||||
|
||||
value?: Value;
|
||||
cacheValue: boolean;
|
||||
constructor(value?: Value, cacheValue = true) {
|
||||
this.cacheValue = cacheValue;
|
||||
if (this.cacheValue) this.value = value;
|
||||
constructor(value?: T) {
|
||||
this.observable = new ControlledObservable();
|
||||
|
||||
this.value = value;
|
||||
this.subscribe = this.observable.subscribe.bind(this.observable);
|
||||
}
|
||||
|
||||
next(value: Value) {
|
||||
if (this.value === value) return;
|
||||
|
||||
if (this.cacheValue) this.value = value;
|
||||
for (const [listener, ctx] of this.listeners) {
|
||||
if (ctx) listener.call(ctx, value);
|
||||
else listener(value);
|
||||
}
|
||||
return this;
|
||||
next(v: T) {
|
||||
this.value = v;
|
||||
this.observable.next(v);
|
||||
}
|
||||
error(err: any) {
|
||||
this.observable.error(err);
|
||||
}
|
||||
|
||||
private findListener(callback: ListenerFn<Value>, ctx?: Object) {
|
||||
return this.listeners.find((l) => {
|
||||
return l[0] === callback && l[1] === ctx;
|
||||
[Symbol.observable]() {
|
||||
return this.observable;
|
||||
}
|
||||
subscribe: Observable<T>["subscribe"];
|
||||
|
||||
map<R>(callback: (value: T) => R, defaultValue?: R): Subject<R> {
|
||||
const child = new Subject(defaultValue);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(listener: ListenerFn<Value>, ctx?: Object, initCall = true) {
|
||||
if (!this.findListener(listener, ctx)) {
|
||||
this.listeners.push([listener, ctx]);
|
||||
|
||||
if (initCall) {
|
||||
if (this.value !== undefined) {
|
||||
if (ctx) listener.call(ctx, this.value);
|
||||
else listener(this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
unsubscribe(listener: ListenerFn<Value>, ctx?: Object) {
|
||||
const entry = this.findListener(listener, ctx);
|
||||
if (entry) {
|
||||
this.listeners = this.listeners.filter((l) => l !== entry);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
get hasListeners() {
|
||||
return this.listeners.length > 0;
|
||||
}
|
||||
|
||||
upstream = new Map<Connectable<any>, ListenerFn<any>>();
|
||||
|
||||
connect(connectable: Connectable<Value>) {
|
||||
if (!this.upstream.has(connectable)) {
|
||||
const handler = this.next.bind(this);
|
||||
this.upstream.set(connectable, handler);
|
||||
connectable.subscribe(handler, this);
|
||||
|
||||
if (connectable.value !== undefined) {
|
||||
handler(connectable.value);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
connectWithHandler<From>(connectable: Connectable<From>, connection: Connection<From, Value, typeof this.value>) {
|
||||
if (!this.upstream.has(connectable)) {
|
||||
const handler = (value: From) => {
|
||||
connection(value, this.next.bind(this), this.value);
|
||||
};
|
||||
this.upstream.set(connectable, handler);
|
||||
connectable.subscribe(handler, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
disconnect(connectable: Connectable<any>) {
|
||||
const handler = this.upstream.get(connectable);
|
||||
if (handler) {
|
||||
this.upstream.delete(connectable);
|
||||
connectable.unsubscribe(handler, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
disconnectAll() {
|
||||
for (const [connectable, listener] of this.upstream) {
|
||||
this.disconnect(connectable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PersistentSubject<Value> extends Subject<Value> implements ConnectableApi<Value> {
|
||||
value: Value;
|
||||
constructor(value: Value) {
|
||||
super(value, true);
|
||||
export class PersistentSubject<T> extends Subject<T> {
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export default Subject;
|
||||
|
@@ -1,107 +1,25 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Debugger } from "debug";
|
||||
import { Filter, matchFilters } from "nostr-tools";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import { NostrEvent, isATag, isETag } from "../types/nostr-event";
|
||||
import { NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
|
||||
import NostrRequest from "./nostr-request";
|
||||
import { RelayQueryMap } from "../types/nostr-relay";
|
||||
import NostrMultiSubscription from "./nostr-multi-subscription";
|
||||
import Subject, { PersistentSubject } from "./subject";
|
||||
import { PersistentSubject } from "./subject";
|
||||
import { logger } from "../helpers/debug";
|
||||
import EventStore from "./event-store";
|
||||
import { isReplaceable } from "../helpers/nostr/events";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import deleteEventService from "../services/delete-events";
|
||||
import {
|
||||
addQueryToFilter,
|
||||
isFilterEqual,
|
||||
isQueryMapEqual,
|
||||
mapQueryMap,
|
||||
stringifyFilter,
|
||||
} from "../helpers/nostr/filter";
|
||||
import { isReplaceable } from "../helpers/nostr/event";
|
||||
import replaceableEventsService from "../services/replaceable-events";
|
||||
import { mergeFilter, isFilterEqual, isQueryMapEqual, mapQueryMap, stringifyFilter } from "../helpers/nostr/filter";
|
||||
import { localRelay } from "../services/local-relay";
|
||||
import { relayRequest } from "../helpers/relay";
|
||||
import SuperMap from "./super-map";
|
||||
import ChunkedRequest from "./chunked-request";
|
||||
|
||||
const BLOCK_SIZE = 100;
|
||||
|
||||
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
|
||||
|
||||
export class RelayBlockLoader {
|
||||
relay: string;
|
||||
filter: NostrRequestFilter;
|
||||
blockSize = BLOCK_SIZE;
|
||||
private log: Debugger;
|
||||
|
||||
loading = false;
|
||||
events: EventStore;
|
||||
/** set to true when the next block produces 0 events */
|
||||
complete = false;
|
||||
|
||||
onBlockFinish = new Subject<number>();
|
||||
|
||||
constructor(relay: string, filter: NostrRequestFilter, log?: Debugger) {
|
||||
this.relay = relay;
|
||||
this.filter = filter;
|
||||
|
||||
this.log = log || logger.extend(relay);
|
||||
this.events = new EventStore(relay);
|
||||
|
||||
deleteEventService.stream.subscribe(this.handleDeleteEvent, this);
|
||||
}
|
||||
|
||||
loadNextBlock() {
|
||||
this.loading = true;
|
||||
let filter: NostrRequestFilter = addQueryToFilter(this.filter, { limit: this.blockSize });
|
||||
let oldestEvent = this.getLastEvent();
|
||||
if (oldestEvent) {
|
||||
filter = addQueryToFilter(filter, { until: oldestEvent.created_at - 1 });
|
||||
}
|
||||
|
||||
const request = new NostrRequest([this.relay]);
|
||||
|
||||
let gotEvents = 0;
|
||||
request.onEvent.subscribe((e) => {
|
||||
this.handleEvent(e);
|
||||
gotEvents++;
|
||||
});
|
||||
request.onComplete.then(() => {
|
||||
this.loading = false;
|
||||
if (gotEvents === 0) {
|
||||
this.complete = true;
|
||||
this.log("Complete");
|
||||
} else this.log(`Got ${gotEvents} events`);
|
||||
this.onBlockFinish.next(gotEvents);
|
||||
});
|
||||
|
||||
request.start(filter);
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
if (!matchFilters(Array.isArray(this.filter) ? this.filter : [this.filter], event)) return;
|
||||
return this.events.addEvent(event);
|
||||
}
|
||||
|
||||
private handleDeleteEvent(deleteEvent: NostrEvent) {
|
||||
const cord = deleteEvent.tags.find(isATag)?.[1];
|
||||
const eventId = deleteEvent.tags.find(isETag)?.[1];
|
||||
|
||||
if (cord) this.events.deleteEvent(cord);
|
||||
if (eventId) this.events.deleteEvent(eventId);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
deleteEventService.stream.unsubscribe(this.handleDeleteEvent, this);
|
||||
}
|
||||
|
||||
getFirstEvent(nth = 0, eventFilter?: EventFilter) {
|
||||
return this.events.getFirstEvent(nth, eventFilter);
|
||||
}
|
||||
getLastEvent(nth = 0, eventFilter?: EventFilter) {
|
||||
return this.events.getLastEvent(nth, eventFilter);
|
||||
}
|
||||
}
|
||||
|
||||
export default class TimelineLoader {
|
||||
cursor = dayjs().unix();
|
||||
queryMap: RelayQueryMap = {};
|
||||
@@ -118,22 +36,21 @@ export default class TimelineLoader {
|
||||
private log: Debugger;
|
||||
private subscription: NostrMultiSubscription;
|
||||
|
||||
private blockLoaders = new Map<string, RelayBlockLoader>();
|
||||
private chunkLoaders = new Map<string, ChunkedRequest>();
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.log = logger.extend("TimelineLoader:" + name);
|
||||
this.events = new EventStore(name);
|
||||
this.events.connect(replaceableEventsService.events, false);
|
||||
|
||||
this.subscription = new NostrMultiSubscription(name);
|
||||
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
||||
this.subscription.onEvent.subscribe(this.handleEvent.bind(this));
|
||||
|
||||
// update the timeline when there are new events
|
||||
this.events.onEvent.subscribe(this.throttleUpdateTimeline, this);
|
||||
this.events.onDelete.subscribe(this.throttleUpdateTimeline, this);
|
||||
this.events.onClear.subscribe(this.throttleUpdateTimeline, this);
|
||||
|
||||
deleteEventService.stream.subscribe(this.handleDeleteEvent, this);
|
||||
this.events.onEvent.subscribe(this.throttleUpdateTimeline.bind(this));
|
||||
this.events.onDelete.subscribe(this.throttleUpdateTimeline.bind(this));
|
||||
this.events.onClear.subscribe(this.throttleUpdateTimeline.bind(this));
|
||||
}
|
||||
|
||||
private throttleUpdateTimeline = _throttle(this.updateTimeline, 10);
|
||||
@@ -145,29 +62,28 @@ export default class TimelineLoader {
|
||||
}
|
||||
private handleEvent(event: NostrEvent, cache = true) {
|
||||
// if this is a replaceable event, mirror it over to the replaceable event service
|
||||
if (isReplaceable(event.kind)) replaceableEventLoaderService.handleEvent(event);
|
||||
if (isReplaceable(event.kind)) replaceableEventsService.handleEvent(event);
|
||||
|
||||
this.events.addEvent(event);
|
||||
if (cache) localRelay.publish(event);
|
||||
}
|
||||
private handleDeleteEvent(deleteEvent: NostrEvent) {
|
||||
const cord = deleteEvent.tags.find(isATag)?.[1];
|
||||
const eventId = deleteEvent.tags.find(isETag)?.[1];
|
||||
|
||||
if (cord) this.events.deleteEvent(cord);
|
||||
if (eventId) this.events.deleteEvent(eventId);
|
||||
private handleChunkFinished() {
|
||||
this.updateLoading();
|
||||
this.updateComplete();
|
||||
}
|
||||
|
||||
private connectToBlockLoader(loader: RelayBlockLoader) {
|
||||
private chunkLoaderSubs = new SuperMap<ChunkedRequest, ZenObservable.Subscription[]>(() => []);
|
||||
private connectToChunkLoader(loader: ChunkedRequest) {
|
||||
this.events.connect(loader.events);
|
||||
loader.onBlockFinish.subscribe(this.updateLoading, this);
|
||||
loader.onBlockFinish.subscribe(this.updateComplete, this);
|
||||
const subs = this.chunkLoaderSubs.get(loader);
|
||||
subs.push(loader.onChunkFinish.subscribe(this.handleChunkFinished.bind(this)));
|
||||
}
|
||||
private disconnectToBlockLoader(loader: RelayBlockLoader) {
|
||||
private disconnectToChunkLoader(loader: ChunkedRequest) {
|
||||
loader.cleanup();
|
||||
this.events.disconnect(loader.events);
|
||||
loader.onBlockFinish.unsubscribe(this.updateLoading, this);
|
||||
loader.onBlockFinish.unsubscribe(this.updateComplete, this);
|
||||
const subs = this.chunkLoaderSubs.get(loader);
|
||||
for (const sub of subs) sub.unsubscribe();
|
||||
this.chunkLoaderSubs.delete(loader);
|
||||
}
|
||||
|
||||
private loadQueriesFromCache(queryMap: RelayQueryMap) {
|
||||
@@ -191,26 +107,26 @@ export default class TimelineLoader {
|
||||
|
||||
// remove relays
|
||||
for (const relay of Object.keys(this.queryMap)) {
|
||||
const loader = this.blockLoaders.get(relay);
|
||||
const loader = this.chunkLoaders.get(relay);
|
||||
if (!loader) continue;
|
||||
if (!queryMap[relay]) {
|
||||
this.disconnectToBlockLoader(loader);
|
||||
this.blockLoaders.delete(relay);
|
||||
this.disconnectToChunkLoader(loader);
|
||||
this.chunkLoaders.delete(relay);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [relay, filter] of Object.entries(queryMap)) {
|
||||
// remove outdated loaders
|
||||
if (this.queryMap[relay] && !isFilterEqual(this.queryMap[relay], filter)) {
|
||||
const old = this.blockLoaders.get(relay)!;
|
||||
this.disconnectToBlockLoader(old);
|
||||
this.blockLoaders.delete(relay);
|
||||
const old = this.chunkLoaders.get(relay)!;
|
||||
this.disconnectToChunkLoader(old);
|
||||
this.chunkLoaders.delete(relay);
|
||||
}
|
||||
|
||||
if (!this.blockLoaders.has(relay)) {
|
||||
const loader = new RelayBlockLoader(relay, filter, this.log.extend(relay));
|
||||
this.blockLoaders.set(relay, loader);
|
||||
this.connectToBlockLoader(loader);
|
||||
if (!this.chunkLoaders.has(relay)) {
|
||||
const loader = new ChunkedRequest(relay, Array.isArray(filter) ? filter : [filter], this.log.extend(relay));
|
||||
this.chunkLoaders.set(relay, loader);
|
||||
this.connectToChunkLoader(loader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,10 +137,10 @@ export default class TimelineLoader {
|
||||
|
||||
// update the subscription query map and add limit
|
||||
this.subscription.setQueryMap(
|
||||
mapQueryMap(this.queryMap, (filter) => addQueryToFilter(filter, { limit: BLOCK_SIZE / 2 })),
|
||||
mapQueryMap(this.queryMap, (filter) => mergeFilter(filter, { limit: BLOCK_SIZE / 2 })),
|
||||
);
|
||||
|
||||
this.triggerBlockLoads();
|
||||
this.triggerChunkLoad();
|
||||
}
|
||||
|
||||
setEventFilter(filter?: EventFilter) {
|
||||
@@ -233,33 +149,33 @@ export default class TimelineLoader {
|
||||
}
|
||||
setCursor(cursor: number) {
|
||||
this.cursor = cursor;
|
||||
this.triggerBlockLoads();
|
||||
this.triggerChunkLoad();
|
||||
}
|
||||
|
||||
triggerBlockLoads() {
|
||||
triggerChunkLoad() {
|
||||
let triggeredLoad = false;
|
||||
for (const [relay, loader] of this.blockLoaders) {
|
||||
for (const [relay, loader] of this.chunkLoaders) {
|
||||
if (loader.complete || loader.loading) continue;
|
||||
const event = loader.getLastEvent(this.loadNextBlockBuffer, this.eventFilter);
|
||||
if (!event || event.created_at >= this.cursor) {
|
||||
loader.loadNextBlock();
|
||||
loader.loadNextChunk();
|
||||
triggeredLoad = true;
|
||||
}
|
||||
}
|
||||
if (triggeredLoad) this.updateLoading();
|
||||
}
|
||||
loadNextBlock() {
|
||||
loadAllNextChunks() {
|
||||
let triggeredLoad = false;
|
||||
for (const [relay, loader] of this.blockLoaders) {
|
||||
for (const [relay, loader] of this.chunkLoaders) {
|
||||
if (loader.complete || loader.loading) continue;
|
||||
loader.loadNextBlock();
|
||||
loader.loadNextChunk();
|
||||
triggeredLoad = true;
|
||||
}
|
||||
if (triggeredLoad) this.updateLoading();
|
||||
}
|
||||
|
||||
private updateLoading() {
|
||||
for (const [relay, loader] of this.blockLoaders) {
|
||||
for (const [relay, loader] of this.chunkLoaders) {
|
||||
if (loader.loading) {
|
||||
if (!this.loading.value) {
|
||||
this.loading.next(true);
|
||||
@@ -270,7 +186,7 @@ export default class TimelineLoader {
|
||||
if (this.loading.value) this.loading.next(false);
|
||||
}
|
||||
private updateComplete() {
|
||||
for (const [relay, loader] of this.blockLoaders) {
|
||||
for (const [relay, loader] of this.chunkLoaders) {
|
||||
if (!loader.complete) {
|
||||
this.complete.next(false);
|
||||
return;
|
||||
@@ -292,8 +208,8 @@ export default class TimelineLoader {
|
||||
}
|
||||
reset() {
|
||||
this.cursor = dayjs().unix();
|
||||
for (const [_, loader] of this.blockLoaders) this.disconnectToBlockLoader(loader);
|
||||
this.blockLoaders.clear();
|
||||
for (const [_, loader] of this.chunkLoaders) this.disconnectToChunkLoader(loader);
|
||||
this.chunkLoaders.clear();
|
||||
this.forgetEvents();
|
||||
}
|
||||
|
||||
@@ -301,11 +217,9 @@ export default class TimelineLoader {
|
||||
cleanup() {
|
||||
this.close();
|
||||
|
||||
for (const [_, loader] of this.blockLoaders) this.disconnectToBlockLoader(loader);
|
||||
this.blockLoaders.clear();
|
||||
for (const [_, loader] of this.chunkLoaders) this.disconnectToChunkLoader(loader);
|
||||
this.chunkLoaders.clear();
|
||||
|
||||
this.events.cleanup();
|
||||
|
||||
deleteEventService.stream.unsubscribe(this.handleDeleteEvent, this);
|
||||
}
|
||||
}
|
||||
|
188
src/components/app-handler-modal/index.tsx
Normal file
188
src/components/app-handler-modal/index.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Link,
|
||||
LinkBox,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent, kinds, nip19 } from "nostr-tools";
|
||||
import { encodeDecodeResult } from "../../helpers/nip19";
|
||||
import { ExternalLinkIcon } from "../icons";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { Kind0ParsedContent, getUserDisplayName, parseMetadataContent } from "../../helpers/nostr/user-metadata";
|
||||
import { MetadataAvatar } from "../user/user-avatar";
|
||||
import HoverLinkOverlay from "../hover-link-overlay";
|
||||
import ArrowRight from "../icons/arrow-right";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider, {
|
||||
useRegisterIntersectionEntity,
|
||||
} from "../../providers/local/intersection-observer";
|
||||
import { getEventUID } from "nostr-idb";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import { CopyIconButton } from "../copy-icon-button";
|
||||
|
||||
function useEventFromDecode(decoded: nip19.DecodeResult) {
|
||||
switch (decoded.type) {
|
||||
case "note":
|
||||
return useSingleEvent(decoded.data);
|
||||
case "nevent":
|
||||
return useSingleEvent(decoded.data.id, decoded.data.relays);
|
||||
case "naddr":
|
||||
return useReplaceableEvent(decoded.data, decoded.data.relays);
|
||||
}
|
||||
}
|
||||
function getKindFromDecoded(decoded: nip19.DecodeResult) {
|
||||
switch (decoded.type) {
|
||||
case "naddr":
|
||||
return decoded.data.kind;
|
||||
case "nevent":
|
||||
return decoded.data.kind;
|
||||
case "note":
|
||||
return kinds.ShortTextNote;
|
||||
case "nprofile":
|
||||
return kinds.Metadata;
|
||||
case "npub":
|
||||
return kinds.Metadata;
|
||||
}
|
||||
}
|
||||
|
||||
function AppHandler({ app, decoded }: { app: NostrEvent; decoded: nip19.DecodeResult }) {
|
||||
const metadata = useMemo(() => parseMetadataContent(app), [app]);
|
||||
const link = useMemo(() => {
|
||||
const tag = app.tags.find((t) => t[0] === "web" && t[2] === decoded.type) || app.tags.find((t) => t[0] === "web");
|
||||
return tag ? tag[1].replace("<bech32>", encodeDecodeResult(decoded)) : undefined;
|
||||
}, [decoded, app]);
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(app));
|
||||
|
||||
if (!link) return null;
|
||||
return (
|
||||
<Flex as={LinkBox} gap="2" py="2" px="4" alignItems="center" ref={ref} overflow="hidden" shrink={0}>
|
||||
<MetadataAvatar metadata={metadata} />
|
||||
<Box overflow="hidden">
|
||||
<HoverLinkOverlay fontWeight="bold" href={link} isExternal>
|
||||
{getUserDisplayName(metadata, app.pubkey)}
|
||||
</HoverLinkOverlay>
|
||||
<Text noOfLines={3}>{metadata.about}</Text>
|
||||
</Box>
|
||||
<ArrowRight boxSize={6} ml="auto" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppHandlerModal({
|
||||
decoded,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: { decoded: nip19.DecodeResult } & Omit<ModalProps, "children">) {
|
||||
const readRelays = useReadRelays();
|
||||
const event = useEventFromDecode(decoded);
|
||||
const kind = event?.kind ?? getKindFromDecoded(decoded);
|
||||
const alt = event?.tags.find((t) => t[0] === "alt")?.[1];
|
||||
const address = encodeDecodeResult(decoded);
|
||||
const timeline = useTimelineLoader(
|
||||
`${kind}-apps`,
|
||||
readRelays,
|
||||
kind ? { kinds: [kinds.Handlerinformation], "#k": [String(kind)] } : { kinds: [kinds.Handlerinformation] },
|
||||
);
|
||||
|
||||
const autofocus = useBreakpointValue({ base: false, lg: true });
|
||||
const [search, setSearch] = useState("");
|
||||
const apps = useSubject(timeline.timeline).filter((a) => a.content.length > 0);
|
||||
|
||||
const filteredApps = apps.filter((app) => {
|
||||
if (search.length > 1) {
|
||||
try {
|
||||
const parsed = JSON.parse(app.content) as Kind0ParsedContent;
|
||||
if (getUserDisplayName(parsed, app.pubkey).toLowerCase().includes(search.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
return false;
|
||||
} else return true;
|
||||
});
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader p="4">{kind === 0 ? `View profile in` : `View event (k:${kind}) in`}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody display="flex" gap="2" flexDirection="column" p="0">
|
||||
{alt && (
|
||||
<Text fontStyle="italic" px="4">
|
||||
{alt}
|
||||
</Text>
|
||||
)}
|
||||
{apps.length > 4 && (
|
||||
<Box px="4">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search apps"
|
||||
autoFocus={autofocus}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex gap="2" direction="column" overflowX="hidden" overflowY="auto" maxH="sm">
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{filteredApps.map((app) => (
|
||||
<AppHandler decoded={decoded} app={app} key={app.id} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
</Flex>
|
||||
|
||||
<FormControl px="4">
|
||||
<FormLabel>Embed Code</FormLabel>
|
||||
<Flex gap="2" overflow="hidden">
|
||||
<Input readOnly value={"nostr:" + address} size="sm" />
|
||||
<CopyIconButton value={"nostr:" + address} size="sm" aria-label="Copy embed code" />
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl px="4">
|
||||
<FormLabel>Share URL</FormLabel>
|
||||
<Flex gap="2" overflow="hidden">
|
||||
<Input readOnly value={"https://njump.me/" + address} size="sm" />
|
||||
<CopyIconButton value={"https://njump.me/" + address} size="sm" aria-label="Copy embed code" />
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter display="flex" gap="2" p="4">
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
variant="outline"
|
||||
href={`https://nostrapp.link/#${address}?select=true`}
|
||||
isExternal
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
colorScheme="primary"
|
||||
>
|
||||
nostrapp.link
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@@ -3,11 +3,11 @@ import { useEffect, useState } from "react";
|
||||
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link } from "@chakra-ui/react";
|
||||
import { getDecodedToken, Token, CashuMint } from "@cashu/cashu-ts";
|
||||
|
||||
import { CopyIconButton } from "./copy-icon-button";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import { ECashIcon, WalletIcon } from "./icons";
|
||||
import { getMint } from "../services/cashu-mints";
|
||||
import { CopyIconButton } from "../copy-icon-button";
|
||||
import useUserMetadata from "../../hooks/use-user-metadata";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { ECashIcon, WalletIcon } from "../icons";
|
||||
import { getMint } from "../../services/cashu-mints";
|
||||
|
||||
function RedeemButton({ token }: { token: string }) {
|
||||
const account = useCurrentAccount()!;
|
||||
@@ -60,7 +60,7 @@ export default function InlineCachuCard({ token, ...props }: Omit<CardProps, "ch
|
||||
</Box>
|
||||
{cashu.memo && <Box>Memo: {cashu.memo}</Box>}
|
||||
<ButtonGroup ml="auto">
|
||||
<CopyIconButton text={token} title="Copy Token" aria-label="Copy Token" />
|
||||
<CopyIconButton value={token} title="Copy Token" aria-label="Copy Token" />
|
||||
<IconButton
|
||||
as={Link}
|
||||
icon={<WalletIcon />}
|
@@ -1,9 +1,9 @@
|
||||
import { memo } from "react";
|
||||
import { verifyEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { CheckIcon, VerificationFailed } from "./icons";
|
||||
import useAppSettings from "../hooks/use-app-settings";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { CheckIcon, VerificationFailed } from "../icons";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
|
||||
function EventVerificationIcon({ event }: { event: NostrEvent }) {
|
||||
const { showSignatureVerification } = useAppSettings();
|
@@ -1,24 +1,20 @@
|
||||
import { Link, MenuItem } from "@chakra-ui/react";
|
||||
import { MenuItem } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
|
||||
import { ExternalLinkIcon } from "../icons";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import { useCallback, useContext, useMemo } from "react";
|
||||
import { AppHandlerContext } from "../../providers/route/app-handler-provider";
|
||||
|
||||
export default function OpenInAppMenuItem({ event }: { event: NostrEvent }) {
|
||||
const address = getSharableEventAddress(event);
|
||||
const address = useMemo(() => getSharableEventAddress(event), [event]);
|
||||
const { openAddress } = useContext(AppHandlerContext);
|
||||
const open = useCallback(() => address && openAddress(address), [address, openAddress]);
|
||||
|
||||
if (!address) return null;
|
||||
return (
|
||||
address && (
|
||||
<MenuItem
|
||||
as={Link}
|
||||
href={buildAppSelectUrl(address)}
|
||||
icon={<ExternalLinkIcon />}
|
||||
isExternal
|
||||
textDecoration="none !important"
|
||||
>
|
||||
View in app...
|
||||
</MenuItem>
|
||||
)
|
||||
<MenuItem icon={<ExternalLinkIcon />} onClick={open}>
|
||||
View in app...
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
@@ -15,9 +15,9 @@ import {
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import UserAvatar from "./user-avatar";
|
||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import UserAvatar from "./user/user-avatar";
|
||||
import { getUserDisplayName } from "../helpers/nostr/user-metadata";
|
||||
import useUserMetadata from "../hooks/use-user-metadata";
|
||||
|
||||
function UserTag({ pubkey, ...props }: { pubkey: string } & Omit<TagProps, "children">) {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
|
@@ -1,90 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Td,
|
||||
Tr,
|
||||
Th,
|
||||
Flex,
|
||||
ButtonProps,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { useInterval } from "react-use";
|
||||
import { RelayStatus } from "./relay-status";
|
||||
import { RelayIcon } from "./icons";
|
||||
import Relay from "../classes/relay";
|
||||
import { RelayFavicon } from "./relay-favicon";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
import { RelayScoreBreakdown } from "./relay-score-breakdown";
|
||||
|
||||
export const ConnectedRelays = ({ ...props }: Omit<ButtonProps, "children">) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
|
||||
const sortedRelays = useMemo(() => relayScoreboardService.getRankedRelays(relays.map((r) => r.url)), [relays]);
|
||||
|
||||
useInterval(() => {
|
||||
setRelays(relayPoolService.getRelays());
|
||||
}, 1000);
|
||||
|
||||
const connected = relays.filter((relay) => relay.okay);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onOpen} leftIcon={<RelayIcon />} {...props}>
|
||||
connected to {connected.length} relays
|
||||
</Button>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="5xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader pb="0">Connected Relays</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody p="2">
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Relay</Th>
|
||||
<Th>Claims</Th>
|
||||
<Th>Score</Th>
|
||||
<Th>Status</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{sortedRelays.map((url) => (
|
||||
<Tr key={url}>
|
||||
<Td>
|
||||
<Flex alignItems="center" maxW="sm" overflow="hidden">
|
||||
<RelayFavicon size="xs" relay={url} mr="2" />
|
||||
<Text>{url}</Text>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{relayPoolService.getRelayClaims(url).size}</Td>
|
||||
<Td>
|
||||
<RelayScoreBreakdown relay={url} />
|
||||
</Td>
|
||||
<Td>
|
||||
<RelayStatus url={url} />
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -3,7 +3,7 @@ import { IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { CheckIcon, CopyToClipboardIcon } from "./icons";
|
||||
|
||||
export const CopyIconButton = ({ text, ...props }: { text?: string } & Omit<IconButtonProps, "icon">) => {
|
||||
export const CopyIconButton = ({ value, ...props }: { value?: string } & Omit<IconButtonProps, "icon">) => {
|
||||
const toast = useToast();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -11,11 +11,11 @@ export const CopyIconButton = ({ text, ...props }: { text?: string } & Omit<Icon
|
||||
<IconButton
|
||||
icon={copied ? <CheckIcon /> : <CopyToClipboardIcon />}
|
||||
onClick={() => {
|
||||
if (text && navigator.clipboard && !copied) {
|
||||
navigator.clipboard.writeText(text);
|
||||
if (value && navigator.clipboard && !copied) {
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} else toast({ description: text, isClosable: true, duration: null });
|
||||
} else toast({ description: value, isClosable: true, duration: null });
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
@@ -23,7 +23,7 @@ import {
|
||||
import { ModalProps } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { getContentTagRefs, getEventUID, getThreadReferences } from "../../helpers/nostr/events";
|
||||
import { getContentTagRefs, getEventUID, getThreadReferences } from "../../helpers/nostr/event";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import RawValue from "./raw-value";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
@@ -94,7 +94,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
|
||||
<Section
|
||||
label="Content"
|
||||
p="0"
|
||||
actions={<CopyIconButton aria-label="copy json" text={event.content} size="sm" />}
|
||||
actions={<CopyIconButton aria-label="copy json" value={event.content} size="sm" />}
|
||||
>
|
||||
<Code whiteSpace="pre" overflowX="auto" width="100%" p="4">
|
||||
{event.content}
|
||||
@@ -103,7 +103,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
|
||||
<Section
|
||||
label="JSON"
|
||||
p="0"
|
||||
actions={<CopyIconButton aria-label="copy json" text={JSON.stringify(event)} size="sm" />}
|
||||
actions={<CopyIconButton aria-label="copy json" value={JSON.stringify(event)} size="sm" />}
|
||||
>
|
||||
<JsonCode data={event} />
|
||||
</Section>
|
||||
|
@@ -4,11 +4,11 @@ import { NostrEvent, nip19 } from "nostr-tools";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { Tag, isATag, isETag, isPTag } from "../../types/nostr-event";
|
||||
import { aTagToAddressPointer, eTagToEventPointer } from "../../helpers/nostr/events";
|
||||
import { aTagToAddressPointer, eTagToEventPointer } from "../../helpers/nostr/event";
|
||||
import { EmbedEventPointer } from "../embed-event";
|
||||
import UserAvatarLink from "../user-avatar-link";
|
||||
import UserLink from "../user-link";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||
import UserAvatarLink from "../user/user-avatar-link";
|
||||
import UserLink from "../user/user-link";
|
||||
import { UserDnsIdentityIcon } from "../user/user-dns-identity-icon";
|
||||
|
||||
function EventTag({ tag }: { tag: Tag }) {
|
||||
const expand = useDisclosure();
|
||||
|
@@ -11,7 +11,7 @@ export default function RawValue({ value, heading }: { heading: string; value?:
|
||||
<Code fontSize="md" wordBreak="break-all">
|
||||
{value}
|
||||
</Code>
|
||||
<CopyIconButton text={String(value)} size="xs" aria-label="copy" />
|
||||
<CopyIconButton value={String(value)} size="xs" aria-label="copy" />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -2,18 +2,18 @@ import { Flex, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay }
|
||||
import { ModalProps } from "@chakra-ui/react";
|
||||
import { kinds, nip19 } from "nostr-tools";
|
||||
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import useUserMetadata from "../../hooks/use-user-metadata";
|
||||
import RawValue from "./raw-value";
|
||||
import RawJson from "./raw-json";
|
||||
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
|
||||
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
||||
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
|
||||
import replaceableEventsService from "../../services/replaceable-events";
|
||||
|
||||
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const nprofile = useSharableProfileId(pubkey);
|
||||
const relays = replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey).value;
|
||||
const relays = replaceableEventsService.getEvent(kinds.RelayList, pubkey).value;
|
||||
const tipMetadata = useUserLNURLMetadata(pubkey);
|
||||
|
||||
return (
|
||||
|
@@ -5,7 +5,7 @@ export type MenuIconButtonProps = IconButtonProps & {
|
||||
children: MenuListProps["children"];
|
||||
};
|
||||
|
||||
export function CustomMenuIconButton({ children, icon, ...props }: MenuIconButtonProps) {
|
||||
export function DotsMenuButton({ children, icon, ...props }: MenuIconButtonProps) {
|
||||
return (
|
||||
<Menu isLazy>
|
||||
<MenuButton as={IconButton} icon={icon || <MoreIcon />} {...props} />
|
@@ -1,4 +1,17 @@
|
||||
import { Box, Card, CardBody, CardProps, Flex, Image, LinkBox, LinkOverlay, Tag, Text } from "@chakra-ui/react";
|
||||
import { useContext } from "react";
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardBody,
|
||||
CardProps,
|
||||
Flex,
|
||||
Heading,
|
||||
Image,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
Tag,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import {
|
||||
getArticleImage,
|
||||
@@ -7,11 +20,11 @@ import {
|
||||
getArticleTitle,
|
||||
} from "../../../helpers/nostr/long-form";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
|
||||
|
||||
export default function EmbeddedArticle({ article, ...props }: Omit<CardProps, "children"> & { article: NostrEvent }) {
|
||||
const title = getArticleTitle(article);
|
||||
@@ -19,9 +32,10 @@ export default function EmbeddedArticle({ article, ...props }: Omit<CardProps, "
|
||||
const summary = getArticleSummary(article);
|
||||
|
||||
const naddr = getSharableEventAddress(article);
|
||||
const { openAddress } = useContext(AppHandlerContext);
|
||||
|
||||
return (
|
||||
<Card as={LinkBox} size="sm" {...props}>
|
||||
<Card as={LinkBox} size="sm" onClick={() => naddr && openAddress(naddr)} cursor="pointer" {...props}>
|
||||
{image && (
|
||||
<Box
|
||||
backgroundImage={image}
|
||||
@@ -42,9 +56,7 @@ export default function EmbeddedArticle({ article, ...props }: Omit<CardProps, "
|
||||
<UserLink pubkey={article.pubkey} fontWeight="bold" isTruncated />
|
||||
<Timestamp timestamp={getArticlePublishDate(article) ?? article.created_at} />
|
||||
</Flex>
|
||||
<LinkOverlay href={naddr ? buildAppSelectUrl(naddr, false) : undefined} isExternal fontWeight="bold">
|
||||
{title}
|
||||
</LinkOverlay>
|
||||
<Heading size="md">{title}</Heading>
|
||||
<Text mb="2">{summary}</Text>
|
||||
{article.tags
|
||||
.filter((t) => t[0] === "t" && t[1])
|
||||
|
@@ -12,8 +12,8 @@ import {
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import UserLink from "../../../components/user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getBadgeDescription, getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
|
||||
|
@@ -2,8 +2,8 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { Box, Card, CardBody, CardFooter, CardHeader, CardProps, Flex, Heading, LinkBox, Text } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import useChannelMetadata from "../../../hooks/use-channel-metadata";
|
||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
|
@@ -2,8 +2,8 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { Card, CardFooter, CardHeader, CardProps, Heading, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import UserLink from "../../../components/user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities";
|
||||
|
||||
|
@@ -2,8 +2,8 @@ import { Card, CardBody, CardHeader, CardProps, IconButton, LinkBox, Text, useDi
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { TrustProvider } from "../../../providers/local/trust";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import DecryptPlaceholder from "../../../views/dms/components/decrypt-placeholder";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
|
@@ -15,8 +15,8 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { getEmojisFromPack, getPackName } from "../../../helpers/nostr/emoji-packs";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import EmojiPackFavoriteButton from "../../../views/emoji-packs/components/emoji-pack-favorite-button";
|
||||
import EmojiPackMenu from "../../../views/emoji-packs/components/emoji-pack-menu";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
@@ -46,7 +46,7 @@ export default function EmbeddedEmojiPack({ pack, ...props }: Omit<CardProps, "c
|
||||
{emojis.length > 0 && (
|
||||
<Flex mb="2" wrap="wrap" gap="2">
|
||||
{emojis.map(({ name, url }) => (
|
||||
<Image key={name + url} src={url} title={name} w={8} h={8} />
|
||||
<Image key={name + url} src={url} title={name} alt={`:${name}:`} w={8} h={8} overflow="hidden" />
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
|
@@ -2,8 +2,8 @@ import { Card, CardBody, CardProps, Flex, Heading, Image, Link, Text } from "@ch
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatar from "../../user-avatar";
|
||||
import UserLink from "../../user/user-link";
|
||||
import UserAvatar from "../../user/user-avatar";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import { getVideoDuration, getVideoImages, getVideoSummary, getVideoTitle } from "../../../helpers/nostr/flare";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
|
@@ -4,8 +4,8 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getGoalName } from "../../../helpers/nostr/goal";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import GoalProgress from "../../../views/goals/components/goal-progress";
|
||||
import GoalZapButton from "../../../views/goals/components/goal-zap-button";
|
||||
import GoalTopZappers from "../../../views/goals/components/goal-top-zappers";
|
||||
|
@@ -3,12 +3,12 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getListDescription, getListName, isSpecialListKind } from "../../../helpers/nostr/lists";
|
||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import ListFeedButton from "../../../views/lists/components/list-feed-button";
|
||||
import { ListCardContent } from "../../../views/lists/components/list-card";
|
||||
import { createCoordinate } from "../../../classes/batch-kind-loader";
|
||||
|
||||
export default function EmbeddedList({ list, ...props }: Omit<CardProps, "children"> & { list: NostrEvent }) {
|
||||
const link = isSpecialListKind(list.kind) ? createCoordinate(list.kind, list.pubkey) : getSharableEventAddress(list);
|
||||
|
@@ -3,14 +3,14 @@ import { Card, CardProps, Flex, LinkBox, Spacer } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../user/user-dns-identity-icon";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../../event-verification-icon";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import { TrustProvider } from "../../../providers/local/trust";
|
||||
import { NoteLink } from "../../note-link";
|
||||
import { NoteLink } from "../../note/note-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
|
@@ -2,11 +2,11 @@ import { Card, CardProps, Flex, LinkBox, Spacer, Text } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { TrustProvider } from "../../../providers/local/trust";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import Timestamp from "../../timestamp";
|
||||
import ReactionIcon from "../../event-reactions/reaction-icon";
|
||||
import { NoteLink } from "../../note-link";
|
||||
import { NoteLink } from "../../note/note-link";
|
||||
import { nip25 } from "nostr-tools";
|
||||
|
||||
export default function EmbeddedReaction({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
|
@@ -12,17 +12,17 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
import { getHashtags } from "../../../helpers/nostr/stemstr";
|
||||
import { ReplyIcon } from "../../icons";
|
||||
import NoteZapButton from "../../note/note-zap-button";
|
||||
import QuoteRepostButton from "../../note/components/quote-repost-button";
|
||||
import Timestamp from "../../timestamp";
|
||||
import TrackStemstrButton from "../../../views/tracks/components/track-stemstr-button";
|
||||
import TrackDownloadButton from "../../../views/tracks/components/track-download-button";
|
||||
import TrackPlayer from "../../../views/tracks/components/track-player";
|
||||
import QuoteRepostButton from "../../note/quote-repost-button";
|
||||
import NoteZapButton from "../../note/note-zap-button";
|
||||
|
||||
// example nevent1qqst32cnyhhs7jt578u7vp3y047dduuwjquztpvwqc43f3nvg8dh28gpzamhxue69uhhyetvv9ujuum5v4khxarj9eshquq4rxdxa
|
||||
export default function EmbeddedStemstrTrack({ track, ...props }: Omit<CardProps, "children"> & { track: NostrEvent }) {
|
||||
|
@@ -2,8 +2,8 @@ import { Box, Card, CardProps, Divider, Flex, Link, Text } from "@chakra-ui/reac
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { NostrEvent, isATag } from "../../../types/nostr-event";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatar from "../../user-avatar";
|
||||
import UserLink from "../../user/user-link";
|
||||
import UserAvatar from "../../user/user-avatar";
|
||||
import ChatMessageContent from "../../../views/streams/stream/stream-chat/chat-message-content";
|
||||
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||
import { parseStreamEvent } from "../../../helpers/nostr/stream";
|
||||
|
@@ -4,8 +4,8 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { parseStreamEvent } from "../../../helpers/nostr/stream";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import StreamStatusBadge from "../../../views/streams/components/status-badge";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatar from "../../user-avatar";
|
||||
import UserLink from "../../user/user-link";
|
||||
import UserAvatar from "../../user/user-avatar";
|
||||
import useEventNaddr from "../../../hooks/use-event-naddr";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
|
@@ -2,16 +2,16 @@ import { Card, CardProps, Flex, LinkBox, Spacer, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../../event-verification-icon";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import { TrustProvider } from "../../../providers/local/trust";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
import { getThreadReferences } from "../../../helpers/nostr/events";
|
||||
import { getThreadReferences } from "../../../helpers/nostr/event";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { getTorrentTitle } from "../../../helpers/nostr/torrents";
|
||||
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
|
||||
|
@@ -17,8 +17,8 @@ import {
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import Timestamp from "../../timestamp";
|
||||
import Magnet from "../../icons/magnet";
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { MouseEventHandler, useCallback, useMemo } from "react";
|
||||
import { Box, Button, ButtonGroup, Card, CardBody, CardHeader, CardProps, Link, Text } from "@chakra-ui/react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { Box, Button, ButtonGroup, Card, CardBody, CardHeader, CardProps, Text } from "@chakra-ui/react";
|
||||
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../user/user-dns-identity-icon";
|
||||
import {
|
||||
embedEmoji,
|
||||
embedNostrHashtags,
|
||||
@@ -21,9 +20,11 @@ import { ExternalLinkIcon } from "../../icons";
|
||||
import { renderAudioUrl } from "../../embed-types/audio";
|
||||
import DebugEventButton from "../../debug-modal/debug-event-button";
|
||||
import DebugEventTags from "../../debug-modal/event-tags";
|
||||
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
|
||||
|
||||
export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const address = getSharableEventAddress(event);
|
||||
const { openAddress } = useContext(AppHandlerContext);
|
||||
|
||||
const alt = event.tags.find((t) => t[0] === "alt")?.[1];
|
||||
const content = useMemo(() => {
|
||||
@@ -47,15 +48,11 @@ export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "ch
|
||||
<Text>kind: {event.kind}</Text>
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
<ButtonGroup ml="auto">
|
||||
<Button
|
||||
as={Link}
|
||||
size="sm"
|
||||
leftIcon={<ExternalLinkIcon />}
|
||||
isExternal
|
||||
href={address ? buildAppSelectUrl(address) : ""}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
{address && (
|
||||
<Button size="sm" leftIcon={<ExternalLinkIcon />} onClick={() => openAddress(address)}>
|
||||
Open
|
||||
</Button>
|
||||
)}
|
||||
<DebugEventButton event={event} size="sm" variant="outline" />
|
||||
</ButtonGroup>
|
||||
</CardHeader>
|
||||
|
@@ -2,7 +2,7 @@ import { lazy } from "react";
|
||||
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
|
||||
import { getMatchCashu } from "../../helpers/regexp";
|
||||
|
||||
const InlineCachuCard = lazy(() => import("../inline-cashu-card"));
|
||||
const InlineCachuCard = lazy(() => import("../cashu/inline-cashu-card"));
|
||||
|
||||
export function embedCashuTokens(content: EmbedableContent) {
|
||||
return embedJSX(content, {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Link } from "@chakra-ui/react";
|
||||
|
||||
import OpenGraphCard from "../open-graph-card";
|
||||
import OpenGraphLink from "../open-graph-link";
|
||||
import OpenGraphCard from "../open-graph/open-graph-card";
|
||||
import OpenGraphLink from "../open-graph/open-graph-link";
|
||||
|
||||
export function renderGenericUrl(match: URL) {
|
||||
return (
|
||||
|
@@ -17,6 +17,8 @@ export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNo
|
||||
display="inline-block"
|
||||
verticalAlign="middle"
|
||||
title={match[1]}
|
||||
alt={":" + match[1] + ":"}
|
||||
overflow="hidden"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { MouseEventHandler, MutableRefObject, forwardRef, useCallback, useMemo, useRef } from "react";
|
||||
import { Image, ImageProps, Link, LinkProps } from "@chakra-ui/react";
|
||||
|
||||
import { useTrusted } from "../../providers/local/trust";
|
||||
import { useTrustContext } from "../../providers/local/trust";
|
||||
import { EmbedableContent, defaultGetLocation } from "../../helpers/embeds";
|
||||
import { getMatchLink } from "../../helpers/regexp";
|
||||
import { useRegisterSlide } from "../lightbox-provider";
|
||||
@@ -10,15 +10,14 @@ import PhotoGallery, { PhotoWithoutSize } from "../photo-gallery";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import useElementBlur from "../../hooks/use-element-blur";
|
||||
import useElementTrustBlur from "../../hooks/use-element-trust-blur";
|
||||
import { buildImageProxyURL } from "../../helpers/image";
|
||||
|
||||
export type TrustImageProps = ImageProps;
|
||||
|
||||
export const TrustImage = forwardRef<HTMLImageElement, TrustImageProps>((props, ref) => {
|
||||
const { blurImages } = useAppSettings();
|
||||
const trusted = useTrusted();
|
||||
const { onClick, style } = useElementBlur(!trusted);
|
||||
const { onClick, style } = useElementTrustBlur();
|
||||
|
||||
const handleClick = useCallback<MouseEventHandler<HTMLImageElement>>(
|
||||
(e) => {
|
||||
|
@@ -10,3 +10,5 @@ export * from "./cashu";
|
||||
export * from "./video";
|
||||
export * from "./simplex";
|
||||
export * from "./reddit";
|
||||
export * from "./model";
|
||||
export * from "./audio";
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
|
||||
import { InlineInvoiceCard } from "../inline-invoice-card";
|
||||
import { InlineInvoiceCard } from "../lightning/inline-invoice-card";
|
||||
|
||||
export function embedLightningInvoice(content: EmbedableContent) {
|
||||
return embedJSX(content, {
|
||||
|
@@ -3,7 +3,7 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import UserLink from "../user-link";
|
||||
import UserLink from "../user/user-link";
|
||||
import { getMatchHashtag, getMatchNostrLink, stripInvisibleChar } from "../../helpers/regexp";
|
||||
import { safeDecode } from "../../helpers/nip19";
|
||||
import { EmbedEventPointer } from "../embed-event";
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { isVideoURL } from "../../helpers/url";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
import useElementBlur from "../../hooks/use-element-blur";
|
||||
import { useTrusted } from "../../providers/local/trust";
|
||||
import useElementTrustBlur from "../../hooks/use-element-trust-blur";
|
||||
import { useTrustContext } from "../../providers/local/trust";
|
||||
|
||||
const StyledVideo = styled.video`
|
||||
max-width: 30rem;
|
||||
@@ -14,8 +14,7 @@ const StyledVideo = styled.video`
|
||||
|
||||
function TrustVideo({ src }: { src: string }) {
|
||||
const { blurImages } = useAppSettings();
|
||||
const trusted = useTrusted();
|
||||
const { onClick, handleEvent, style } = useElementBlur(!trusted);
|
||||
const { onClick, handleEvent, style } = useElementTrustBlur();
|
||||
|
||||
return (
|
||||
<StyledVideo
|
||||
|
@@ -15,15 +15,15 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import UserAvatarLink from "../user-avatar-link";
|
||||
import UserLink from "../user-link";
|
||||
import UserAvatarLink from "../user/user-avatar-link";
|
||||
import UserLink from "../user/user-link";
|
||||
import { LightningIcon } from "../icons";
|
||||
import { ParsedZap } from "../../helpers/nostr/zaps";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
import useEventZaps from "../../hooks/use-event-zaps";
|
||||
import Timestamp from "../timestamp";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { getEventUID } from "../../helpers/nostr/event";
|
||||
import ReactionDetails from "./reaction-details";
|
||||
import RepostDetails from "./repost-details";
|
||||
|
||||
|
@@ -3,8 +3,8 @@ import { useMemo } from "react";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { groupReactions } from "../../helpers/nostr/reactions";
|
||||
import UserAvatarLink from "../user-avatar-link";
|
||||
import UserLink from "../user-link";
|
||||
import UserAvatarLink from "../user/user-avatar-link";
|
||||
import UserLink from "../user/user-link";
|
||||
import ReactionIcon from "../event-reactions/reaction-icon";
|
||||
|
||||
function ShowMoreGrid({
|
||||
|
@@ -2,8 +2,8 @@ import { Flex, Text } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import UserAvatarLink from "../user-avatar-link";
|
||||
import UserLink from "../user-link";
|
||||
import UserAvatarLink from "../user/user-avatar-link";
|
||||
import UserLink from "../user/user-link";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
@@ -4,6 +4,6 @@ import { DislikeIcon, LikeIcon } from "../icons";
|
||||
export default function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
|
||||
if (emoji === "+") return <LikeIcon />;
|
||||
if (emoji === "-") return <DislikeIcon />;
|
||||
if (url) return <Image src={url} title={emoji} alt={emoji} w="1em" h="1em" display="inline" />;
|
||||
if (url) return <Image src={url} title={emoji} alt={emoji} w="1em" h="1em" display="inline" overflow="hidden" />;
|
||||
return <span>{emoji}</span>;
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import clientRelaysService from "../../services/client-relays";
|
||||
import { getZapSplits } from "../../helpers/nostr/zaps";
|
||||
import { unique } from "../../helpers/array";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import { getEventCoordinate, isReplaceable } from "../../helpers/nostr/events";
|
||||
import { getEventCoordinate, isReplaceable } from "../../helpers/nostr/event";
|
||||
import { EmbedProps } from "../embed-event";
|
||||
import userMailboxesService from "../../services/user-mailboxes";
|
||||
import InputStep from "./input-step";
|
||||
@@ -26,7 +26,7 @@ import signingService from "../../services/signing";
|
||||
import accountService from "../../services/account";
|
||||
import PayStep from "./pay-step";
|
||||
import { getInvoiceFromCallbackUrl } from "../../helpers/lnurl";
|
||||
import UserLink from "../user-link";
|
||||
import UserLink from "../user/user-link";
|
||||
import relayHintService from "../../services/event-relay-hint";
|
||||
|
||||
export type PayRequest = { invoice?: string; pubkey: string; error?: any };
|
||||
|
@@ -9,8 +9,8 @@ import { getZapSplits } from "../../helpers/nostr/zaps";
|
||||
import { EmbedEvent, EmbedProps } from "../embed-event";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
import CustomZapAmountOptions from "./zap-options";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import UserLink from "../user-link";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import UserLink from "../user/user-link";
|
||||
|
||||
function UserCard({ pubkey, percent }: { pubkey: string; percent?: number }) {
|
||||
const { address } = useUserLNURLMetadata(pubkey);
|
||||
|
@@ -2,8 +2,8 @@ import { useMount } from "react-use";
|
||||
import { Alert, Button, ButtonGroup, Flex, IconButton, Spacer, useDisclosure, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { PayRequest } from ".";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import UserLink from "../user-link";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import UserLink from "../user/user-link";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CheckIcon, ErrorIcon, LightningIcon } from "../icons";
|
||||
import { InvoiceModalContent } from "../invoice-modal";
|
||||
import { PropsWithChildren, useEffect, useState } from "react";
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import { ExternalLinkIcon, QrCodeIcon } from "./icons";
|
||||
import QrCodeSvg from "./qr-code-svg";
|
||||
import QrCodeSvg from "./qr-code/qr-code-svg";
|
||||
import { CopyIconButton } from "./copy-icon-button";
|
||||
|
||||
type CommonProps = { invoice: string; onPaid: () => void };
|
||||
@@ -67,7 +67,7 @@ export function InvoiceModalContent({ invoice, onPaid }: CommonProps) {
|
||||
variant="solid"
|
||||
size="md"
|
||||
/>
|
||||
<CopyIconButton text={invoice} aria-label="Copy Invoice" variant="solid" size="md" />
|
||||
<CopyIconButton value={invoice} aria-label="Copy Invoice" variant="solid" size="md" />
|
||||
</Flex>
|
||||
<Flex gap="2">
|
||||
{window.webln && (
|
||||
|
@@ -2,12 +2,12 @@ import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Box, Button, Flex, IconButton, Text, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import { getUserDisplayName } from "../../helpers/nostr/user-metadata";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import useUserMetadata from "../../hooks/use-user-metadata";
|
||||
import accountService, { Account } from "../../services/account";
|
||||
import { AddIcon, ChevronDownIcon, ChevronUpIcon } from "../icons";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import AccountInfoBadge from "../account-info-badge";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
|
||||
|
@@ -8,8 +8,8 @@ import dayjs from "dayjs";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import accountService from "../../services/account";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import UserLink from "../user-link";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import UserLink from "../user/user-link";
|
||||
import { GhostIcon } from "../icons";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
|
@@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { PostModalContext } from "../../providers/route/post-modal-provider";
|
||||
import { DirectMessagesIcon, NotesIcon, NotificationsIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
||||
import UserAvatar from "../user-avatar";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import MobileSideDrawer from "./mobile-side-drawer";
|
||||
import Rocket02 from "../icons/rocket-02";
|
||||
|
||||
|
@@ -30,9 +30,9 @@ declare module "yet-another-react-lightbox" {
|
||||
}
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import UserAvatarLink from "./user-avatar-link";
|
||||
import UserLink from "./user-link";
|
||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||
import UserAvatarLink from "./user/user-avatar-link";
|
||||
import UserLink from "./user/user-link";
|
||||
import { UserDnsIdentityIcon } from "./user/user-dns-identity-icon";
|
||||
import styled from "@emotion/styled";
|
||||
import { getSharableEventAddress } from "../helpers/nip19";
|
||||
|
||||
|
@@ -4,8 +4,8 @@ import dayjs from "dayjs";
|
||||
import { requestProvider } from "webln";
|
||||
import { Box, BoxProps, Button, ButtonGroup, IconButton, Text } from "@chakra-ui/react";
|
||||
|
||||
import { parsePaymentRequest, readablizeSats } from "../helpers/bolt11";
|
||||
import { CopyToClipboardIcon } from "./icons";
|
||||
import { parsePaymentRequest, readablizeSats } from "../../helpers/bolt11";
|
||||
import { CopyToClipboardIcon } from "../icons";
|
||||
|
||||
export type InvoiceButtonProps = {
|
||||
paymentRequest: string;
|
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -21,8 +21,7 @@ import { nip19 } from "nostr-tools";
|
||||
import { useSet } from "react-use";
|
||||
|
||||
import { ExternalLinkIcon, SearchIcon } from "./icons";
|
||||
import { buildAppSelectUrl } from "../helpers/nostr/apps";
|
||||
import UserLink from "./user-link";
|
||||
import UserLink from "./user/user-link";
|
||||
import { encodeDecodeResult } from "../helpers/nip19";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
@@ -30,7 +29,8 @@ import { isValidRelayURL } from "../helpers/relay";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
import { RelayFavicon } from "./relay-favicon";
|
||||
import singleEventService from "../services/single-event";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import replaceableEventsService from "../services/replaceable-events";
|
||||
import { AppHandlerContext } from "../providers/route/app-handler-provider";
|
||||
|
||||
function SearchOnRelaysModal({
|
||||
isOpen,
|
||||
@@ -53,7 +53,7 @@ function SearchOnRelaysModal({
|
||||
setLoading(true);
|
||||
switch (decode.type) {
|
||||
case "naddr":
|
||||
replaceableEventLoaderService.requestEvent(
|
||||
replaceableEventsService.requestEvent(
|
||||
Array.from(relays),
|
||||
decode.data.kind,
|
||||
decode.data.pubkey,
|
||||
@@ -126,7 +126,8 @@ function SearchOnRelaysModal({
|
||||
}
|
||||
|
||||
export default function LoadingNostrLink({ link }: { link: nip19.DecodeResult }) {
|
||||
const encoded = encodeDecodeResult(link);
|
||||
const { openAddress } = useContext(AppHandlerContext);
|
||||
const address = encodeDecodeResult(link);
|
||||
const details = useDisclosure();
|
||||
const search = useDisclosure();
|
||||
|
||||
@@ -184,7 +185,7 @@ export default function LoadingNostrLink({ link }: { link: nip19.DecodeResult })
|
||||
>
|
||||
[{details.isOpen ? "-" : "+"}]
|
||||
<Text as="span" isTruncated>
|
||||
{encoded}
|
||||
{address}
|
||||
</Text>
|
||||
</Button>
|
||||
{details.isOpen && (
|
||||
@@ -195,7 +196,7 @@ export default function LoadingNostrLink({ link }: { link: nip19.DecodeResult })
|
||||
<Button leftIcon={<SearchIcon />} colorScheme="primary" onClick={search.onOpen}>
|
||||
Find
|
||||
</Button>
|
||||
<Button as={Link} leftIcon={<ExternalLinkIcon />} href={buildAppSelectUrl(encoded)} isExternal>
|
||||
<Button leftIcon={<ExternalLinkIcon />} onClick={() => openAddress(address)}>
|
||||
Open
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
@@ -12,8 +12,8 @@ import { matchSorter } from "match-sorter";
|
||||
|
||||
import { Emoji, useContextEmojis } from "../providers/global/emoji-provider";
|
||||
import { useUserSearchDirectoryContext } from "../providers/global/user-directory-provider";
|
||||
import UserAvatar from "./user-avatar";
|
||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||
import UserAvatar from "./user/user-avatar";
|
||||
import { UserDnsIdentityIcon } from "./user/user-dns-identity-icon";
|
||||
|
||||
export type PeopleToken = { pubkey: string; names: string[] };
|
||||
type Token = Emoji | PeopleToken;
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { CardProps, Flex } from "@chakra-ui/react";
|
||||
|
||||
import useCurrentAccount from "../hooks/use-current-account";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import MessageBubble, { MessageBubbleProps } from "./message-bubble";
|
||||
import { useThreadsContext } from "../providers/local/thread-provider";
|
||||
import ThreadButton from "../views/dms/components/thread-button";
|
||||
import UserAvatarLink from "./user-avatar-link";
|
||||
import { useThreadsContext } from "../../providers/local/thread-provider";
|
||||
import ThreadButton from "../../views/dms/components/thread-button";
|
||||
import UserAvatarLink from "../user/user-avatar-link";
|
||||
|
||||
function MessageBubbleWithThread({ message, showThreadButton = true, ...props }: MessageBubbleProps) {
|
||||
const { threads } = useThreadsContext();
|
@@ -1,17 +1,17 @@
|
||||
import { ReactNode, useRef } from "react";
|
||||
import { ButtonGroup, Card, CardBody, CardFooter, CardHeader, CardProps } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../providers/local/intersection-observer";
|
||||
import { getEventUID } from "../helpers/nostr/events";
|
||||
import Timestamp from "./timestamp";
|
||||
import NoteZapButton from "./note/note-zap-button";
|
||||
import UserLink from "./user-link";
|
||||
import { UserDnsIdentityIcon } from "./user-dns-identity-icon";
|
||||
import useEventReactions from "../hooks/use-event-reactions";
|
||||
import AddReactionButton from "./note/components/add-reaction-button";
|
||||
import EventReactionButtons from "./event-reactions/event-reactions";
|
||||
import { IconThreadButton } from "../views/dms/components/thread-button";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/local/intersection-observer";
|
||||
import { getEventUID } from "../../helpers/nostr/event";
|
||||
import Timestamp from "../timestamp";
|
||||
import UserLink from "../user/user-link";
|
||||
import { UserDnsIdentityIcon } from "../user/user-dns-identity-icon";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
import EventReactionButtons from "../event-reactions/event-reactions";
|
||||
import { IconThreadButton } from "../../views/dms/components/thread-button";
|
||||
import AddReactionButton from "../note/timeline-note/components/add-reaction-button";
|
||||
import NoteZapButton from "../note/note-zap-button";
|
||||
|
||||
export type MessageBubbleProps = {
|
||||
message: NostrEvent;
|
@@ -12,21 +12,21 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useUserLists from "../../../hooks/use-user-lists";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
import {
|
||||
NOTE_LIST_KIND,
|
||||
listAddEvent,
|
||||
listRemoveEvent,
|
||||
getEventPointersFromList,
|
||||
getListName,
|
||||
} from "../../../helpers/nostr/lists";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getEventCoordinate } from "../../../helpers/nostr/events";
|
||||
import { BookmarkIcon, BookmarkedIcon, PlusCircleIcon } from "../../icons";
|
||||
import NewListModal from "../../../views/lists/components/new-list-modal";
|
||||
import useEventBookmarkActions from "../../../hooks/use-event-bookmark-actions";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
} from "../../helpers/nostr/lists";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { getEventCoordinate } from "../../helpers/nostr/event";
|
||||
import { BookmarkIcon, BookmarkedIcon, PlusCircleIcon } from "../icons";
|
||||
import NewListModal from "../../views/lists/components/new-list-modal";
|
||||
import useEventBookmarkActions from "../../hooks/use-event-bookmark-actions";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
|
||||
export default function BookmarkButton({ event, ...props }: { event: NostrEvent } & Omit<IconButtonProps, "icon">) {
|
||||
const publish = usePublishEvent();
|
@@ -1,18 +1,14 @@
|
||||
import { memo } from "react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { getEventRelays } from "../../services/event-relays";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { RelayIconStack, RelayIconStackProps } from "../relay-icon-stack";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { getEventUID } from "../../helpers/nostr/event";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
|
||||
export type NoteRelaysProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
|
||||
export const EventRelays = memo(
|
||||
({ event, ...props }: NoteRelaysProps & Omit<RelayIconStackProps, "relays" | "maxRelays">) => {
|
||||
({ event, ...props }: { event: NostrEvent } & Omit<RelayIconStackProps, "relays" | "maxRelays">) => {
|
||||
const maxRelays = useBreakpointValue({ base: 3, md: undefined });
|
||||
const eventRelays = useSubject(getEventRelays(getEventUID(event)));
|
||||
|
@@ -2,15 +2,15 @@ import { useMemo } from "react";
|
||||
import { Link, LinkProps } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { truncatedId } from "../helpers/nostr/events";
|
||||
import relayHintService from "../services/event-relay-hint";
|
||||
import { truncatedId } from "../../helpers/nostr/event";
|
||||
import relayHintService from "../../services/event-relay-hint";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
export type NoteLinkProps = LinkProps & {
|
||||
noteId: string;
|
||||
};
|
||||
|
||||
export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => {
|
||||
export function NoteLink({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) {
|
||||
const nevent = useMemo(() => {
|
||||
const relays = relayHintService.getEventPointerRelayHints(noteId).slice(0, 2);
|
||||
return nip19.neventEncode({ id: noteId, relays });
|
||||
@@ -21,4 +21,6 @@ export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: Not
|
||||
{children || truncatedId(nevent)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default NoteLink;
|
@@ -4,7 +4,7 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { BroadcastEventIcon } from "../icons";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
|
||||
import { DotsMenuButton, MenuIconButtonProps } from "../dots-menu-button";
|
||||
import NoteTranslationModal from "../../views/tools/transform-note/translation";
|
||||
import Translate01 from "../icons/translate-01";
|
||||
import InfoCircle from "../icons/info-circle";
|
||||
@@ -33,7 +33,7 @@ export default function NoteMenu({
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomMenuIconButton {...props}>
|
||||
<DotsMenuButton {...props}>
|
||||
<OpenInAppMenuItem event={event} />
|
||||
<CopyShareLinkMenuItem event={event} />
|
||||
<CopyEmbedCodeMenuItem event={event} />
|
||||
@@ -65,7 +65,7 @@ export default function NoteMenu({
|
||||
</MenuItem>
|
||||
)}
|
||||
<DebugEventMenuItem event={event} />
|
||||
</CustomMenuIconButton>
|
||||
</DotsMenuButton>
|
||||
|
||||
{translationsModal.isOpen && <NoteTranslationModal isOpen onClose={translationsModal.onClose} note={event} />}
|
||||
</>
|
||||
|
@@ -10,7 +10,7 @@ import { NostrEvent } from "../../types/nostr-event";
|
||||
import { LightningIcon } from "../icons";
|
||||
import ZapModal from "../event-zap-modal";
|
||||
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { getEventUID } from "../../helpers/nostr/event";
|
||||
|
||||
export type NoteZapButtonProps = Omit<ButtonProps, "children"> & {
|
||||
event: NostrEvent;
|
||||
|
@@ -2,8 +2,8 @@ import { MouseEventHandler, useCallback } from "react";
|
||||
import { IconButton, IconButtonProps } from "@chakra-ui/react";
|
||||
import { To } from "react-router-dom";
|
||||
|
||||
import { DrawerIcon } from "./icons";
|
||||
import { useNavigateInDrawer } from "../providers/drawer-sub-view-provider";
|
||||
import { DrawerIcon } from "../icons";
|
||||
import { useNavigateInDrawer } from "../../providers/drawer-sub-view-provider";
|
||||
|
||||
export default function OpenInDrawerButton({
|
||||
to,
|
@@ -1,10 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { ButtonProps, IconButton } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { QuoteRepostIcon } from "../../icons";
|
||||
import { PostModalContext } from "../../../providers/route/post-modal-provider";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { QuoteRepostIcon } from "../icons";
|
||||
import { PostModalContext } from "../../providers/route/post-modal-provider";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
|
||||
export type QuoteRepostButtonProps = Omit<ButtonProps, "children" | "onClick"> & {
|
||||
event: NostrEvent;
|
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ButtonProps,
|
||||
IconButton,
|
||||
@@ -9,15 +10,14 @@ import {
|
||||
Portal,
|
||||
useBoolean,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { AddReactionIcon } from "../../icons";
|
||||
import ReactionPicker from "../../reaction-picker";
|
||||
import { draftEventReaction } from "../../../helpers/nostr/reactions";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import { useState } from "react";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import useEventReactions from "../../../../hooks/use-event-reactions";
|
||||
import { AddReactionIcon } from "../../../icons";
|
||||
import ReactionPicker from "../../../reaction-picker";
|
||||
import { draftEventReaction } from "../../../../helpers/nostr/reactions";
|
||||
import { getEventUID } from "../../../../helpers/nostr/event";
|
||||
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
||||
|
||||
export default function AddReactionButton({
|
||||
event,
|
@@ -1,10 +1,10 @@
|
||||
import { IconButton, IconButtonProps } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import InfoCircle from "../../icons/info-circle";
|
||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import useEventZaps from "../../../hooks/use-event-zaps";
|
||||
import InfoCircle from "../../../icons/info-circle";
|
||||
import useEventReactions from "../../../../hooks/use-event-reactions";
|
||||
import { getEventUID } from "../../../../helpers/nostr/event";
|
||||
import useEventZaps from "../../../../hooks/use-event-zaps";
|
||||
|
||||
export function NoteDetailsButton({
|
||||
event,
|
@@ -1,7 +1,8 @@
|
||||
import { IconButton, IconButtonProps, Link } from "@chakra-ui/react";
|
||||
import { ExternalLinkIcon } from "../../icons";
|
||||
import { useMemo } from "react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { ExternalLinkIcon } from "../../../icons";
|
||||
|
||||
export default function NoteProxyLink({
|
||||
event,
|
@@ -1,10 +1,10 @@
|
||||
import { ButtonGroup, ButtonGroupProps, Divider } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import AddReactionButton from "./add-reaction-button";
|
||||
import EventReactionButtons from "../../event-reactions/event-reactions";
|
||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import EventReactionButtons from "../../../event-reactions/event-reactions";
|
||||
import { useBreakpointValue } from "../../../../providers/global/breakpoint-provider";
|
||||
import useEventReactions from "../../../../hooks/use-event-reactions";
|
||||
|
||||
export default function NoteReactions({ event, ...props }: Omit<ButtonGroupProps, "children"> & { event: NostrEvent }) {
|
||||
const reactions = useEventReactions(event.id) ?? [];
|
@@ -1,12 +1,12 @@
|
||||
import { Button, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { RepostIcon } from "../../icons";
|
||||
import useEventCount from "../../../hooks/use-event-count";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import { RepostIcon } from "../../../icons";
|
||||
import useEventCount from "../../../../hooks/use-event-count";
|
||||
import useEventExists from "../../../../hooks/use-event-exists";
|
||||
import useCurrentAccount from "../../../../hooks/use-current-account";
|
||||
import RepostModal from "./repost-modal";
|
||||
import useEventExists from "../../../hooks/use-event-exists";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
|
||||
export default function RepostButton({ event }: { event: NostrEvent }) {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
@@ -12,20 +12,19 @@ import {
|
||||
SimpleGrid,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { EventTemplate, NostrEvent, kinds } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import { EmbedEvent } from "../../embed-event";
|
||||
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
|
||||
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||
import relayHintService from "../../../services/event-relay-hint";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../../icons";
|
||||
import relayHintService from "../../../../services/event-relay-hint";
|
||||
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
||||
import useCurrentAccount from "../../../../hooks/use-current-account";
|
||||
import useUserCommunitiesList from "../../../../hooks/use-user-communities-list";
|
||||
import { createCoordinate } from "../../../../classes/batch-kind-loader";
|
||||
import { EmbedEvent } from "../../../embed-event";
|
||||
|
||||
function buildRepost(event: NostrEvent): DraftNostrEvent {
|
||||
function buildRepost(event: NostrEvent): EventTemplate {
|
||||
const hint = relayHintService.getEventRelayHint(event);
|
||||
const tags: NostrEvent["tags"] = [];
|
||||
tags.push(["e", event.id, hint ?? ""]);
|
@@ -14,44 +14,44 @@ import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import UserAvatarLink from "../user-avatar-link";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import NoteMenu from "./note-menu";
|
||||
import UserLink from "../user-link";
|
||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||
import NoteZapButton from "./note-zap-button";
|
||||
import { ExpandProvider } from "../../providers/local/expanded";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import appSettings from "../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../event-verification-icon";
|
||||
import NoteMenu from "../note-menu";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { UserDnsIdentityIcon } from "../../user/user-dns-identity-icon";
|
||||
import NoteZapButton from "../note-zap-button";
|
||||
import { ExpandProvider } from "../../../providers/local/expanded";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import appSettings from "../../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../../common-event/event-verification-icon";
|
||||
import RepostButton from "./components/repost-button";
|
||||
import QuoteRepostButton from "./components/quote-repost-button";
|
||||
import { ReplyIcon } from "../icons";
|
||||
import QuoteRepostButton from "../quote-repost-button";
|
||||
import { ReplyIcon } from "../../icons";
|
||||
import NoteContentWithWarning from "./note-content-with-warning";
|
||||
import { TrustProvider } from "../../providers/local/trust";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/local/intersection-observer";
|
||||
import BookmarkButton from "./components/bookmark-button";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { TrustProvider } from "../../../providers/local/trust";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||
import BookmarkButton from "../bookmark-button";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import NoteReactions from "./components/note-reactions";
|
||||
import ReplyForm from "../../views/thread/components/reply-form";
|
||||
import { getThreadReferences, truncatedId } from "../../helpers/nostr/events";
|
||||
import Timestamp from "../timestamp";
|
||||
import ReplyForm from "../../../views/thread/components/reply-form";
|
||||
import { getThreadReferences, truncatedId } from "../../../helpers/nostr/event";
|
||||
import Timestamp from "../../timestamp";
|
||||
import OpenInDrawerButton from "../open-in-drawer-button";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import HoverLinkOverlay from "../hover-link-overlay";
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||
import NoteCommunityMetadata from "./note-community-metadata";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import { CompactNoteContent } from "../compact-note-content";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
import NoteProxyLink from "./components/note-proxy-link";
|
||||
import { NoteDetailsButton } from "./components/note-details-button";
|
||||
import EventInteractionDetailsModal from "../event-interactions-modal";
|
||||
import singleEventService from "../../services/single-event";
|
||||
import EventInteractionDetailsModal from "../../event-interactions-modal";
|
||||
import singleEventService from "../../../services/single-event";
|
||||
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import POWIcon from "../pow-icon";
|
||||
import POWIcon from "../../pow/pow-icon";
|
||||
|
||||
function ReplyToE({ pointer }: { pointer: EventPointer }) {
|
||||
const event = useSingleEvent(pointer.id, pointer.relays);
|
||||
@@ -111,7 +111,7 @@ export type NoteProps = Omit<CardProps, "children"> & {
|
||||
registerIntersectionEntity?: boolean;
|
||||
clickable?: boolean;
|
||||
};
|
||||
export function Note({
|
||||
export function TimelineNote({
|
||||
event,
|
||||
variant = "outline",
|
||||
showReplyButton,
|
||||
@@ -211,4 +211,4 @@ export function Note({
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Note);
|
||||
export default memo(TimelineNote);
|
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Link, Text, TextProps } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { getEventCommunityPointer } from "../../helpers/nostr/communities";
|
||||
import { getEventCommunityPointer } from "../../../helpers/nostr/communities";
|
||||
|
||||
export default function NoteCommunityMetadata({
|
||||
event,
|
@@ -1,9 +1,9 @@
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { NoteContents } from "./text-note-contents";
|
||||
import { useExpand } from "../../providers/local/expanded";
|
||||
import SensitiveContentWarning from "../sensitive-content-warning";
|
||||
import useAppSettings from "../../hooks/use-app-settings";
|
||||
import { TextNoteContents } from "./text-note-contents";
|
||||
import { useExpand } from "../../../providers/local/expanded";
|
||||
import SensitiveContentWarning from "../../sensitive-content-warning";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
|
||||
export default function NoteContentWithWarning({ event }: { event: NostrEvent }) {
|
||||
const expand = useExpand();
|
||||
@@ -15,6 +15,6 @@ export default function NoteContentWithWarning({ event }: { event: NostrEvent })
|
||||
return showContentWarning ? (
|
||||
<SensitiveContentWarning description={contentWarningTag?.[1]} />
|
||||
) : (
|
||||
<NoteContents px="2" event={event} />
|
||||
<TextNoteContents px="2" event={event} />
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user