Merge branch 'next'

This commit is contained in:
hzrd149
2024-03-08 15:18:59 -06:00
408 changed files with 3701 additions and 2406 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Rebuild observable class

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add "open in" modal (NIP-89)

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add event publisher tool

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Add UI tab to relays

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix custom emoji reactions having multiple colons

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix jsonl database export format

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Dont auto-play blured videos

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Added Event Console tool

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix bunker://pubkey connect URIs

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to automatically decrypt DMs

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix profile form removing unknown metadata fields

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Unblur all images when clicking on a note

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Update emojilib

View File

@@ -1,3 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 120
}

View File

@@ -13,5 +13,6 @@
"webln"
],
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enable": false
}

View File

@@ -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",

View File

@@ -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 /> },
],
},
{

View 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();
}
}
}
}

View 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);
}
}

View 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"];
}

View File

@@ -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) {

View File

@@ -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)) {

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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
View 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;
}
}

View File

@@ -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> {

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View 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>
);
}

View File

@@ -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 />}

View File

@@ -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();

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
</>
);
};

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>
);

View File

@@ -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 (

View File

@@ -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} />

View File

@@ -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])

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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>
)}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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);

View File

@@ -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";

View File

@@ -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 }) {

View File

@@ -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 }) {

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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>

View File

@@ -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, {

View File

@@ -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 (

View File

@@ -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"
/>
);
}

View File

@@ -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) => {

View File

@@ -10,3 +10,5 @@ export * from "./cashu";
export * from "./video";
export * from "./simplex";
export * from "./reddit";
export * from "./model";
export * from "./audio";

View File

@@ -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, {

View File

@@ -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";

View File

@@ -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

View File

@@ -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";

View File

@@ -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({

View File

@@ -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";

View File

@@ -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>;
}

View File

@@ -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 };

View File

@@ -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);

View File

@@ -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";

View File

@@ -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 && (

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();

View File

@@ -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)));

View File

@@ -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;

View File

@@ -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} />}
</>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) ?? [];

View File

@@ -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();

View File

@@ -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 ?? ""]);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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