diff --git a/.changeset/clever-swans-walk.md b/.changeset/clever-swans-walk.md new file mode 100644 index 000000000..6bcaeb946 --- /dev/null +++ b/.changeset/clever-swans-walk.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Rebuild observable class diff --git a/.changeset/famous-tables-repair.md b/.changeset/famous-tables-repair.md new file mode 100644 index 000000000..e5795d53d --- /dev/null +++ b/.changeset/famous-tables-repair.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add "open in" modal (NIP-89) diff --git a/.changeset/fresh-books-dream.md b/.changeset/fresh-books-dream.md new file mode 100644 index 000000000..b04072f9b --- /dev/null +++ b/.changeset/fresh-books-dream.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add event publisher tool diff --git a/.changeset/heavy-carrots-melt.md b/.changeset/heavy-carrots-melt.md new file mode 100644 index 000000000..a29533170 --- /dev/null +++ b/.changeset/heavy-carrots-melt.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Add UI tab to relays diff --git a/.changeset/large-items-knock.md b/.changeset/large-items-knock.md new file mode 100644 index 000000000..fbf23164d --- /dev/null +++ b/.changeset/large-items-knock.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Fix custom emoji reactions having multiple colons diff --git a/.changeset/lovely-dryers-protect.md b/.changeset/lovely-dryers-protect.md new file mode 100644 index 000000000..31c038d99 --- /dev/null +++ b/.changeset/lovely-dryers-protect.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Fix jsonl database export format diff --git a/.changeset/nine-chicken-fetch.md b/.changeset/nine-chicken-fetch.md new file mode 100644 index 000000000..c93a435a8 --- /dev/null +++ b/.changeset/nine-chicken-fetch.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Dont auto-play blured videos diff --git a/.changeset/popular-pandas-wave.md b/.changeset/popular-pandas-wave.md new file mode 100644 index 000000000..906f5ceb4 --- /dev/null +++ b/.changeset/popular-pandas-wave.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Added Event Console tool diff --git a/.changeset/quick-peas-remain.md b/.changeset/quick-peas-remain.md new file mode 100644 index 000000000..087d89157 --- /dev/null +++ b/.changeset/quick-peas-remain.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Fix bunker://pubkey connect URIs diff --git a/.changeset/shiny-ghosts-hear.md b/.changeset/shiny-ghosts-hear.md new file mode 100644 index 000000000..b1ed9b62a --- /dev/null +++ b/.changeset/shiny-ghosts-hear.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add option to automatically decrypt DMs diff --git a/.changeset/soft-trees-check.md b/.changeset/soft-trees-check.md new file mode 100644 index 000000000..96a00a2fd --- /dev/null +++ b/.changeset/soft-trees-check.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Fix profile form removing unknown metadata fields diff --git a/.changeset/strong-camels-whisper.md b/.changeset/strong-camels-whisper.md new file mode 100644 index 000000000..ee0f5eaa0 --- /dev/null +++ b/.changeset/strong-camels-whisper.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Unblur all images when clicking on a note diff --git a/.changeset/violet-adults-share.md b/.changeset/violet-adults-share.md new file mode 100644 index 000000000..2ffad1e35 --- /dev/null +++ b/.changeset/violet-adults-share.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +Update emojilib diff --git a/.prettierrc b/.prettierrc index 963354f23..054d599cf 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,5 @@ { + "tabWidth": 2, + "useTabs": false, "printWidth": 120 } diff --git a/.vscode/settings.json b/.vscode/settings.json index 491303bc7..85c632814 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,6 @@ "webln" ], "typescript.enablePromptUseWorkspaceTsdk": true, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "deno.enable": false } diff --git a/package.json b/package.json index 0586e1983..545aa43ac 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "@chakra-ui/react": "^2.8.2", "@chakra-ui/shared-utils": "^2.0.4", "@chakra-ui/styled-system": "^2.9.2", + "@codemirror/autocomplete": "^6.12.0", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.10.1", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@getalby/bitcoin-connect": "^3.2.1", @@ -31,15 +34,18 @@ "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.2", "@noble/secp256k1": "^1.7.0", + "@uiw/codemirror-theme-github": "^4.21.21", + "@uiw/react-codemirror": "^4.21.21", "@webscopeio/react-textarea-autocomplete": "^4.9.2", "bech32": "^2.0.0", "blurhash": "^2.0.5", "chart.js": "^4.4.1", "cheerio": "^1.0.0-rc.12", "chroma-js": "^2.4.2", + "codemirror-json-schema": "^0.6.1", "dayjs": "^1.11.9", "debug": "^4.3.4", - "emojilib": "2", + "emojilib": "^3", "framer-motion": "^10.16.0", "hls.js": "^1.4.14", "idb": "^8.0.0", @@ -73,7 +79,8 @@ "three-spritetext": "^1.8.1", "three-stdlib": "^2.29.4", "webln": "^0.3.2", - "yet-another-react-lightbox": "^3.15.6" + "yet-another-react-lightbox": "^3.15.6", + "zen-observable": "^0.10.0" }, "devDependencies": { "@changesets/cli": "^2.27.1", @@ -89,6 +96,7 @@ "@types/react-dom": "^18.2.18", "@types/three": "^0.160.0", "@types/webscopeio__react-textarea-autocomplete": "^4.7.5", + "@types/zen-observable": "^0.8.7", "@vitejs/plugin-react": "^4.2.1", "camelcase": "^8.0.0", "prettier": "^3.1.1", diff --git a/src/app.tsx b/src/app.tsx index 9c8c0cc8d..b8decf155 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -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: }, { path: "satellite-cdn", element: }, { path: "unknown", element: }, + { path: "console", element: }, + { path: "publisher", element: }, ], }, { diff --git a/src/classes/batch-kind-loader.ts b/src/classes/batch-kind-loader.ts new file mode 100644 index 000000000..52413671f --- /dev/null +++ b/src/classes/batch-kind-loader.ts @@ -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(); + private requested = new Map(); + + 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 = {}; + + 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(); + } + } + } +} diff --git a/src/classes/chunked-request.ts b/src/classes/chunked-request.ts new file mode 100644 index 000000000..7bf7ee360 --- /dev/null +++ b/src/classes/chunked-request.ts @@ -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(); + + 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); + } +} diff --git a/src/classes/controlled-observable.ts b/src/classes/controlled-observable.ts new file mode 100644 index 000000000..91da6853e --- /dev/null +++ b/src/classes/controlled-observable.ts @@ -0,0 +1,64 @@ +import Observable from "zen-observable"; + +export default class ControlledObservable implements Observable { + private observable: Observable; + private subscriptions = new Set>(); + private _complete = false; + get closed() { + return this._complete; + } + get used() { + return this.subscriptions.size > 0; + } + + constructor(subscriber?: ZenObservable.Subscriber) { + 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["subscribe"]; + map: Observable["map"]; + flatMap: Observable["flatMap"]; + forEach: Observable["forEach"]; + reduce: Observable["reduce"]; + filter: Observable["filter"]; + concat: Observable["concat"]; +} diff --git a/src/classes/event-store.ts b/src/classes/event-store.ts index 54c710f11..d7474d8db 100644 --- a/src/classes/event-store.ts +++ b/src/classes/event-store.ts @@ -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(); 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(undefined, false); - onDelete = new Subject(undefined, false); - onClear = new Subject(undefined, false); + onEvent = new ControlledObservable(); + onDelete = new ControlledObservable(); + onClear = new ControlledObservable(); - private replaceableEventSubs = new Map>(); 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(() => []); + 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) { diff --git a/src/classes/nostr-multi-subscription.ts b/src/classes/nostr-multi-subscription.ts index 2ac063658..5ee74d304 100644 --- a/src/classes/nostr-multi-subscription.ts +++ b/src/classes/nostr-multi-subscription.ts @@ -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(); + onEvent = new ControlledObservable(); seenEvents = new Set(); 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(() => []); /** 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)) { diff --git a/src/classes/nostr-publish-action.ts b/src/classes/nostr-publish-action.ts index 4e2bb8f55..fdcff4db2 100644 --- a/src/classes/nostr-publish-action.ts +++ b/src/classes/nostr-publish-action.ts @@ -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([]); - onResult = new Subject(undefined, false); - onComplete = createDefer(); + results = new PersistentSubject<{ relay: Relay; result: IncomingCommandResult }[]>([]); + + onResult = new ControlledObservable<{ relay: Relay; result: IncomingCommandResult }>(); + onComplete = createDefer<{ relay: Relay; result: IncomingCommandResult }[]>(); private remaining = new Set(); + private relayResultSubs = new SuperMap(() => []); constructor(label: string, relays: Iterable, 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); } } } diff --git a/src/classes/nostr-request.ts b/src/classes/nostr-request.ts index 164c9baee..264d421f3 100644 --- a/src/classes/nostr-request.ts +++ b/src/classes/nostr-request.ts @@ -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; state = NostrRequest.IDLE; - onEvent = new Subject(undefined, false); - onCount = new Subject(undefined, false); + onEvent = new ControlledObservable(); + onCount = new ControlledObservable(); onComplete = createDefer(); seenEvents = new Set(); + private relaySubs: SuperMap = new SuperMap(() => []); + constructor(relayUrls: Iterable, 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; diff --git a/src/classes/nostr-subscription.ts b/src/classes/nostr-subscription.ts index fbd9831e5..237a3a90c 100644 --- a/src/classes/nostr-subscription.ts +++ b/src/classes/nostr-subscription.ts @@ -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(); - onEOSE = new Subject(); - constructor(relayUrl: string | URL, query?: NostrRequestFilter, name?: string) { + onEvent = new ControlledObservable(); + onEOSE = new ControlledObservable(); + + 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; } } diff --git a/src/classes/relay-pool.ts b/src/classes/relay-pool.ts new file mode 100644 index 000000000..0673ea171 --- /dev/null +++ b/src/classes/relay-pool.ts @@ -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(); + onRelayCreated = new Subject(); + + relayClaims = new Map>(); + + 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; + } + + 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; + } +} diff --git a/src/classes/relay-set.ts b/src/classes/relay-set.ts index 0f1d9914f..5ae3f3fc2 100644 --- a/src/classes/relay-set.ts +++ b/src/classes/relay-set.ts @@ -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 { diff --git a/src/classes/relay.ts b/src/classes/relay.ts index ac6abe17b..b2ce6d015 100644 --- a/src/classes/relay.ts +++ b/src/classes/relay.ts @@ -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(WebSocket.CLOSED); - onOpen = new Subject(undefined, false); - onClose = new Subject(undefined, false); - onEvent = new Subject(undefined, false); - onNotice = new Subject(undefined, false); - onCount = new Subject(undefined, false); - onEOSE = new Subject(undefined, false); - onCommandResult = new Subject(undefined, false); ws?: WebSocket; + status = new PersistentSubject(WebSocket.CLOSED); + onOpen = new ControlledObservable(); + onClose = new ControlledObservable(); + + onEvent = new ControlledObservable(); + onNotice = new ControlledObservable(); + onCount = new ControlledObservable(); + onEOSE = new ControlledObservable(); + onCommandResult = new ControlledObservable(); private connectionPromises: Deferred[] = []; @@ -61,7 +51,7 @@ export default class Relay { private ejectTimer?: () => void; private intentionalClose = false; private subscriptionResTimer = new Map 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) { - if (!event.data) return; + handleMessage(message: MessageEvent) { + 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); } } } diff --git a/src/classes/subject.ts b/src/classes/subject.ts index eeebd9f25..6bb593eaa 100644 --- a/src/classes/subject.ts +++ b/src/classes/subject.ts @@ -1,111 +1,62 @@ -export type ListenerFn = (value: T) => void; -interface Connectable { - value?: Value; - subscribe(listener: ListenerFn, ctx?: Object): this; - unsubscribe(listener: ListenerFn, ctx?: Object): this; -} -interface ConnectableApi { - connect(connectable: Connectable): this; - disconnect(connectable: Connectable): this; -} -export type Connection = (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 implements Connectable { - listeners: [ListenerFn, Object | undefined][] = []; +/** An observable that is always open and stores the last value */ +export default class Subject { + private observable: ControlledObservable; + 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, ctx?: Object) { - return this.listeners.find((l) => { - return l[0] === callback && l[1] === ctx; + [Symbol.observable]() { + return this.observable; + } + subscribe: Observable["subscribe"]; + + map(callback: (value: T) => R, defaultValue?: R): Subject { + const child = new Subject(defaultValue); + + this.subscribe((value) => { + try { + child.next(callback(value)); + } catch (e) { + child.error(e); + } + }); + + return child; + } + + /** @deprecated */ + connectWithMapper( + subject: Subject, + map: (value: R, next: (value: T) => void, current: T | undefined) => void, + ): ZenObservable.Subscription { + return subject.subscribe((value) => { + map(value, (v) => this.next(v), this.value); }); } - - subscribe(listener: ListenerFn, 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, 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, ListenerFn>(); - - connect(connectable: Connectable) { - 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(connectable: Connectable, connection: Connection) { - 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) { - 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 extends Subject implements ConnectableApi { - value: Value; - constructor(value: Value) { - super(value, true); +export class PersistentSubject extends Subject { + value: T; + constructor(value: T) { + super(); this.value = value; } } - -export default Subject; diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts index 4a8000635..bb13119d7 100644 --- a/src/classes/timeline-loader.ts +++ b/src/classes/timeline-loader.ts @@ -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(); - - 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(); + private chunkLoaders = new Map(); 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(() => []); + 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); } } diff --git a/src/components/app-handler-modal/index.tsx b/src/components/app-handler-modal/index.tsx new file mode 100644 index 000000000..86c885455 --- /dev/null +++ b/src/components/app-handler-modal/index.tsx @@ -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("", encodeDecodeResult(decoded)) : undefined; + }, [decoded, app]); + + const ref = useRef(null); + useRegisterIntersectionEntity(ref, getEventUID(app)); + + if (!link) return null; + return ( + + + + + {getUserDisplayName(metadata, app.pubkey)} + + {metadata.about} + + + + ); +} + +export default function AppHandlerModal({ + decoded, + isOpen, + onClose, +}: { decoded: nip19.DecodeResult } & Omit) { + 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 ( + + + + {kind === 0 ? `View profile in` : `View event (k:${kind}) in`} + + + {alt && ( + + {alt} + + )} + {apps.length > 4 && ( + + setSearch(e.target.value)} + /> + + )} + + + + {filteredApps.map((app) => ( + + ))} + + + + + Embed Code + + + + + + + Share URL + + + + + + + + + + + + + + ); +} diff --git a/src/components/inline-cashu-card.tsx b/src/components/cashu/inline-cashu-card.tsx similarity index 86% rename from src/components/inline-cashu-card.tsx rename to src/components/cashu/inline-cashu-card.tsx index 6e2bfb22b..dfe24425a 100644 --- a/src/components/inline-cashu-card.tsx +++ b/src/components/cashu/inline-cashu-card.tsx @@ -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 {cashu.memo && Memo: {cashu.memo}} - + } diff --git a/src/components/event-verification-icon.tsx b/src/components/common-event/event-verification-icon.tsx similarity index 71% rename from src/components/event-verification-icon.tsx rename to src/components/common-event/event-verification-icon.tsx index 9cb14c387..8de85a6df 100644 --- a/src/components/event-verification-icon.tsx +++ b/src/components/common-event/event-verification-icon.tsx @@ -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(); diff --git a/src/components/common-menu-items/open-in-app.tsx b/src/components/common-menu-items/open-in-app.tsx index 5caea1377..dab365bc0 100644 --- a/src/components/common-menu-items/open-in-app.tsx +++ b/src/components/common-menu-items/open-in-app.tsx @@ -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 && ( - } - isExternal - textDecoration="none !important" - > - View in app... - - ) + } onClick={open}> + View in app... + ); } diff --git a/src/components/compact-user-stack.tsx b/src/components/compact-user-stack.tsx index 0974dd05c..94f12193a 100644 --- a/src/components/compact-user-stack.tsx +++ b/src/components/compact-user-stack.tsx @@ -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) { const metadata = useUserMetadata(pubkey); diff --git a/src/components/connected-relays.tsx b/src/components/connected-relays.tsx deleted file mode 100644 index 1ce04ae4d..000000000 --- a/src/components/connected-relays.tsx +++ /dev/null @@ -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) => { - const { isOpen, onOpen, onClose } = useDisclosure(); - const [relays, setRelays] = useState(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 ( - <> - - - - - Connected Relays - - - - - - - - - - - - - - {sortedRelays.map((url) => ( - - - - - - - ))} - -
RelayClaimsScoreStatus
- - - {url} - - {relayPoolService.getRelayClaims(url).size} - - - -
-
-
-
-
- - ); -}; diff --git a/src/components/copy-icon-button.tsx b/src/components/copy-icon-button.tsx index ccbbb4524..d630c6b1a 100644 --- a/src/components/copy-icon-button.tsx +++ b/src/components/copy-icon-button.tsx @@ -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) => { +export const CopyIconButton = ({ value, ...props }: { value?: string } & Omit) => { const toast = useToast(); const [copied, setCopied] = useState(false); @@ -11,11 +11,11 @@ export const CopyIconButton = ({ text, ...props }: { text?: string } & Omit : } 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} /> diff --git a/src/components/debug-modal/event-debug-modal.tsx b/src/components/debug-modal/event-debug-modal.tsx index 5c13bcadb..e69ac58e2 100644 --- a/src/components/debug-modal/event-debug-modal.tsx +++ b/src/components/debug-modal/event-debug-modal.tsx @@ -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
} + actions={} > {event.content} @@ -103,7 +103,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
} + actions={} >
diff --git a/src/components/debug-modal/event-tags.tsx b/src/components/debug-modal/event-tags.tsx index ccbc1062d..80db054f2 100644 --- a/src/components/debug-modal/event-tags.tsx +++ b/src/components/debug-modal/event-tags.tsx @@ -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(); diff --git a/src/components/debug-modal/raw-value.tsx b/src/components/debug-modal/raw-value.tsx index ebf55101d..725071429 100644 --- a/src/components/debug-modal/raw-value.tsx +++ b/src/components/debug-modal/raw-value.tsx @@ -11,7 +11,7 @@ export default function RawValue({ value, heading }: { heading: string; value?: {value} - + ); diff --git a/src/components/debug-modal/user-debug-modal.tsx b/src/components/debug-modal/user-debug-modal.tsx index e6ec9dbc2..7ba533d0d 100644 --- a/src/components/debug-modal/user-debug-modal.tsx +++ b/src/components/debug-modal/user-debug-modal.tsx @@ -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) { 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 ( diff --git a/src/components/menu-icon-button.tsx b/src/components/dots-menu-button.tsx similarity index 82% rename from src/components/menu-icon-button.tsx rename to src/components/dots-menu-button.tsx index 0d12fdac9..27201767c 100644 --- a/src/components/menu-icon-button.tsx +++ b/src/components/dots-menu-button.tsx @@ -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 ( } {...props} /> diff --git a/src/components/embed-event/event-types/embedded-article.tsx b/src/components/embed-event/event-types/embedded-article.tsx index 912d620b7..b8f437f6f 100644 --- a/src/components/embed-event/event-types/embedded-article.tsx +++ b/src/components/embed-event/event-types/embedded-article.tsx @@ -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 & { article: NostrEvent }) { const title = getArticleTitle(article); @@ -19,9 +32,10 @@ export default function EmbeddedArticle({ article, ...props }: Omit + naddr && openAddress(naddr)} cursor="pointer" {...props}> {image && ( - - {title} - + {title} {summary} {article.tags .filter((t) => t[0] === "t" && t[1]) diff --git a/src/components/embed-event/event-types/embedded-badge.tsx b/src/components/embed-event/event-types/embedded-badge.tsx index bce7fcf6c..1a557855b 100644 --- a/src/components/embed-event/event-types/embedded-badge.tsx +++ b/src/components/embed-event/event-types/embedded-badge.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-channel.tsx b/src/components/embed-event/event-types/embedded-channel.tsx index 8fd13bdce..71bbc4367 100644 --- a/src/components/embed-event/event-types/embedded-channel.tsx +++ b/src/components/embed-event/event-types/embedded-channel.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-community.tsx b/src/components/embed-event/event-types/embedded-community.tsx index 99d9d2e23..45cdc25e2 100644 --- a/src/components/embed-event/event-types/embedded-community.tsx +++ b/src/components/embed-event/event-types/embedded-community.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-dm.tsx b/src/components/embed-event/event-types/embedded-dm.tsx index 47636d6b0..eb2728aab 100644 --- a/src/components/embed-event/event-types/embedded-dm.tsx +++ b/src/components/embed-event/event-types/embedded-dm.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-emoji-pack.tsx b/src/components/embed-event/event-types/embedded-emoji-pack.tsx index bbf6b75f4..8aa981edb 100644 --- a/src/components/embed-event/event-types/embedded-emoji-pack.tsx +++ b/src/components/embed-event/event-types/embedded-emoji-pack.tsx @@ -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 0 && ( {emojis.map(({ name, url }) => ( - + {`:${name}:`} ))} )} diff --git a/src/components/embed-event/event-types/embedded-flare-video.tsx b/src/components/embed-event/event-types/embedded-flare-video.tsx index a978ef3fa..eb88db4c1 100644 --- a/src/components/embed-event/event-types/embedded-flare-video.tsx +++ b/src/components/embed-event/event-types/embedded-flare-video.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-goal.tsx b/src/components/embed-event/event-types/embedded-goal.tsx index d9b207fe2..92a4b56c7 100644 --- a/src/components/embed-event/event-types/embedded-goal.tsx +++ b/src/components/embed-event/event-types/embedded-goal.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-list.tsx b/src/components/embed-event/event-types/embedded-list.tsx index 981a87f3e..394e8264c 100644 --- a/src/components/embed-event/event-types/embedded-list.tsx +++ b/src/components/embed-event/event-types/embedded-list.tsx @@ -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 & { list: NostrEvent }) { const link = isSpecialListKind(list.kind) ? createCoordinate(list.kind, list.pubkey) : getSharableEventAddress(list); diff --git a/src/components/embed-event/event-types/embedded-note.tsx b/src/components/embed-event/event-types/embedded-note.tsx index 1b7089515..9f9e71fb6 100644 --- a/src/components/embed-event/event-types/embedded-note.tsx +++ b/src/components/embed-event/event-types/embedded-note.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-reaction.tsx b/src/components/embed-event/event-types/embedded-reaction.tsx index 469b6a2fe..4d408dab9 100644 --- a/src/components/embed-event/event-types/embedded-reaction.tsx +++ b/src/components/embed-event/event-types/embedded-reaction.tsx @@ -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 & { event: NostrEvent }) { diff --git a/src/components/embed-event/event-types/embedded-stemstr-track.tsx b/src/components/embed-event/event-types/embedded-stemstr-track.tsx index fa696cae2..1f2d779b5 100644 --- a/src/components/embed-event/event-types/embedded-stemstr-track.tsx +++ b/src/components/embed-event/event-types/embedded-stemstr-track.tsx @@ -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 & { track: NostrEvent }) { diff --git a/src/components/embed-event/event-types/embedded-stream-message.tsx b/src/components/embed-event/event-types/embedded-stream-message.tsx index 2e83ddcdb..3d8fbf319 100644 --- a/src/components/embed-event/event-types/embedded-stream-message.tsx +++ b/src/components/embed-event/event-types/embedded-stream-message.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-stream.tsx b/src/components/embed-event/event-types/embedded-stream.tsx index 5720f8c58..51c106622 100644 --- a/src/components/embed-event/event-types/embedded-stream.tsx +++ b/src/components/embed-event/event-types/embedded-stream.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-torrent-comment.tsx b/src/components/embed-event/event-types/embedded-torrent-comment.tsx index ac5b80f5e..92fe3c19e 100644 --- a/src/components/embed-event/event-types/embedded-torrent-comment.tsx +++ b/src/components/embed-event/event-types/embedded-torrent-comment.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-torrent.tsx b/src/components/embed-event/event-types/embedded-torrent.tsx index 3ff5c20b1..e2dd0189e 100644 --- a/src/components/embed-event/event-types/embedded-torrent.tsx +++ b/src/components/embed-event/event-types/embedded-torrent.tsx @@ -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"; diff --git a/src/components/embed-event/event-types/embedded-unknown.tsx b/src/components/embed-event/event-types/embedded-unknown.tsx index 9cade5a6c..1f6197e8d 100644 --- a/src/components/embed-event/event-types/embedded-unknown.tsx +++ b/src/components/embed-event/event-types/embedded-unknown.tsx @@ -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 & { 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 }: Omitkind: {event.kind} - + {address && ( + + )} diff --git a/src/components/embed-types/cashu.tsx b/src/components/embed-types/cashu.tsx index a52e9dad5..0b4b2df1b 100644 --- a/src/components/embed-types/cashu.tsx +++ b/src/components/embed-types/cashu.tsx @@ -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, { diff --git a/src/components/embed-types/common.tsx b/src/components/embed-types/common.tsx index 8f43305d5..47fc45494 100644 --- a/src/components/embed-types/common.tsx +++ b/src/components/embed-types/common.tsx @@ -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 ( diff --git a/src/components/embed-types/emoji.tsx b/src/components/embed-types/emoji.tsx index 0923ab902..4651858bd 100644 --- a/src/components/embed-types/emoji.tsx +++ b/src/components/embed-types/emoji.tsx @@ -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" /> ); } diff --git a/src/components/embed-types/image.tsx b/src/components/embed-types/image.tsx index de36e03a6..5d5c55e0a 100644 --- a/src/components/embed-types/image.tsx +++ b/src/components/embed-types/image.tsx @@ -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((props, ref) => { const { blurImages } = useAppSettings(); - const trusted = useTrusted(); - const { onClick, style } = useElementBlur(!trusted); + const { onClick, style } = useElementTrustBlur(); const handleClick = useCallback>( (e) => { diff --git a/src/components/embed-types/index.ts b/src/components/embed-types/index.ts index b87ee716e..d16d2dee9 100644 --- a/src/components/embed-types/index.ts +++ b/src/components/embed-types/index.ts @@ -10,3 +10,5 @@ export * from "./cashu"; export * from "./video"; export * from "./simplex"; export * from "./reddit"; +export * from "./model"; +export * from "./audio"; diff --git a/src/components/embed-types/lightning.tsx b/src/components/embed-types/lightning.tsx index 6528bfcc9..08818f020 100644 --- a/src/components/embed-types/lightning.tsx +++ b/src/components/embed-types/lightning.tsx @@ -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, { diff --git a/src/components/embed-types/nostr.tsx b/src/components/embed-types/nostr.tsx index 228109960..0a94856cd 100644 --- a/src/components/embed-types/nostr.tsx +++ b/src/components/embed-types/nostr.tsx @@ -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"; diff --git a/src/components/embed-types/video.tsx b/src/components/embed-types/video.tsx index 266fbdadf..75dbcbded 100644 --- a/src/components/embed-types/video.tsx +++ b/src/components/embed-types/video.tsx @@ -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 ( ; if (emoji === "-") return ; - if (url) return {emoji}; + if (url) return {emoji}; return {emoji}; } diff --git a/src/components/event-zap-modal/index.tsx b/src/components/event-zap-modal/index.tsx index aa8322473..46f8e213a 100644 --- a/src/components/event-zap-modal/index.tsx +++ b/src/components/event-zap-modal/index.tsx @@ -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 }; diff --git a/src/components/event-zap-modal/input-step.tsx b/src/components/event-zap-modal/input-step.tsx index b708f8771..37140fb39 100644 --- a/src/components/event-zap-modal/input-step.tsx +++ b/src/components/event-zap-modal/input-step.tsx @@ -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); diff --git a/src/components/event-zap-modal/pay-step.tsx b/src/components/event-zap-modal/pay-step.tsx index 5b694f0f8..2434d3213 100644 --- a/src/components/event-zap-modal/pay-step.tsx +++ b/src/components/event-zap-modal/pay-step.tsx @@ -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"; diff --git a/src/components/invoice-modal.tsx b/src/components/invoice-modal.tsx index b8e14a3ab..dc2942e63 100644 --- a/src/components/invoice-modal.tsx +++ b/src/components/invoice-modal.tsx @@ -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" /> - + {window.webln && ( diff --git a/src/components/layout/account-switcher.tsx b/src/components/layout/account-switcher.tsx index f9b78048e..69a9ea6c6 100644 --- a/src/components/layout/account-switcher.tsx +++ b/src/components/layout/account-switcher.tsx @@ -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"; diff --git a/src/components/layout/ghost-toolbar.tsx b/src/components/layout/ghost-toolbar.tsx index 2c6fe0047..023263752 100644 --- a/src/components/layout/ghost-toolbar.tsx +++ b/src/components/layout/ghost-toolbar.tsx @@ -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"; diff --git a/src/components/layout/mobile-bottom-nav.tsx b/src/components/layout/mobile-bottom-nav.tsx index 5461b0c2b..7fe54224e 100644 --- a/src/components/layout/mobile-bottom-nav.tsx +++ b/src/components/layout/mobile-bottom-nav.tsx @@ -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"; diff --git a/src/components/lightbox-provider.tsx b/src/components/lightbox-provider.tsx index cd3f60704..3b562f8c1 100644 --- a/src/components/lightbox-provider.tsx +++ b/src/components/lightbox-provider.tsx @@ -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"; diff --git a/src/components/inline-invoice-card.tsx b/src/components/lightning/inline-invoice-card.tsx similarity index 94% rename from src/components/inline-invoice-card.tsx rename to src/components/lightning/inline-invoice-card.tsx index bb4d1a930..7d40eba01 100644 --- a/src/components/inline-invoice-card.tsx +++ b/src/components/lightning/inline-invoice-card.tsx @@ -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; diff --git a/src/components/loading-nostr-link.tsx b/src/components/loading-nostr-link.tsx index aed17f3e5..9b0e2c1bb 100644 --- a/src/components/loading-nostr-link.tsx +++ b/src/components/loading-nostr-link.tsx @@ -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 ? "-" : "+"}] - {encoded} + {address} {details.isOpen && ( @@ -195,7 +196,7 @@ export default function LoadingNostrLink({ link }: { link: nip19.DecodeResult }) - diff --git a/src/components/magic-textarea.tsx b/src/components/magic-textarea.tsx index 8f108e229..8ee86c0a4 100644 --- a/src/components/magic-textarea.tsx +++ b/src/components/magic-textarea.tsx @@ -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; diff --git a/src/components/message-block.tsx b/src/components/message/message-block.tsx similarity index 86% rename from src/components/message-block.tsx rename to src/components/message/message-block.tsx index d7ef8dc18..c9dea4989 100644 --- a/src/components/message-block.tsx +++ b/src/components/message/message-block.tsx @@ -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(); diff --git a/src/components/message-bubble.tsx b/src/components/message/message-bubble.tsx similarity index 77% rename from src/components/message-bubble.tsx rename to src/components/message/message-bubble.tsx index fe5f91e1b..5ee08f935 100644 --- a/src/components/message-bubble.tsx +++ b/src/components/message/message-bubble.tsx @@ -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; diff --git a/src/components/note/components/bookmark-button.tsx b/src/components/note/bookmark-button.tsx similarity index 86% rename from src/components/note/components/bookmark-button.tsx rename to src/components/note/bookmark-button.tsx index 3846501ac..debc22508 100644 --- a/src/components/note/components/bookmark-button.tsx +++ b/src/components/note/bookmark-button.tsx @@ -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) { const publish = usePublishEvent(); diff --git a/src/components/note/note-relays.tsx b/src/components/note/event-relays.tsx similarity index 68% rename from src/components/note/note-relays.tsx rename to src/components/note/event-relays.tsx index 3ca3479c8..804130960 100644 --- a/src/components/note/note-relays.tsx +++ b/src/components/note/event-relays.tsx @@ -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) => { + ({ event, ...props }: { event: NostrEvent } & Omit) => { const maxRelays = useBreakpointValue({ base: 3, md: undefined }); const eventRelays = useSubject(getEventRelays(getEventUID(event))); diff --git a/src/components/note-link.tsx b/src/components/note/note-link.tsx similarity index 70% rename from src/components/note-link.tsx rename to src/components/note/note-link.tsx index dfc1e42d0..7c6842156 100644 --- a/src/components/note-link.tsx +++ b/src/components/note/note-link.tsx @@ -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)} ); -}; +} + +export default NoteLink; diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx index 107c9083c..40768f06c 100644 --- a/src/components/note/note-menu.tsx +++ b/src/components/note/note-menu.tsx @@ -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 ( <> - + @@ -65,7 +65,7 @@ export default function NoteMenu({ )} - + {translationsModal.isOpen && } diff --git a/src/components/note/note-zap-button.tsx b/src/components/note/note-zap-button.tsx index 5b6d6e666..d60b6efdf 100644 --- a/src/components/note/note-zap-button.tsx +++ b/src/components/note/note-zap-button.tsx @@ -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 & { event: NostrEvent; diff --git a/src/components/open-in-drawer-button.tsx b/src/components/note/open-in-drawer-button.tsx similarity index 85% rename from src/components/open-in-drawer-button.tsx rename to src/components/note/open-in-drawer-button.tsx index 3276de234..44025d172 100644 --- a/src/components/open-in-drawer-button.tsx +++ b/src/components/note/open-in-drawer-button.tsx @@ -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, diff --git a/src/components/note/components/quote-repost-button.tsx b/src/components/note/quote-repost-button.tsx similarity index 74% rename from src/components/note/components/quote-repost-button.tsx rename to src/components/note/quote-repost-button.tsx index 82ad8d287..fcab1c6b8 100644 --- a/src/components/note/components/quote-repost-button.tsx +++ b/src/components/note/quote-repost-button.tsx @@ -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 & { event: NostrEvent; diff --git a/src/components/note/components/add-reaction-button.tsx b/src/components/note/timeline-note/components/add-reaction-button.tsx similarity index 77% rename from src/components/note/components/add-reaction-button.tsx rename to src/components/note/timeline-note/components/add-reaction-button.tsx index 86bf56bf0..9a83723df 100644 --- a/src/components/note/components/add-reaction-button.tsx +++ b/src/components/note/timeline-note/components/add-reaction-button.tsx @@ -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, diff --git a/src/components/note/components/note-details-button.tsx b/src/components/note/timeline-note/components/note-details-button.tsx similarity index 62% rename from src/components/note/components/note-details-button.tsx rename to src/components/note/timeline-note/components/note-details-button.tsx index c211dc79f..6a84ddbee 100644 --- a/src/components/note/components/note-details-button.tsx +++ b/src/components/note/timeline-note/components/note-details-button.tsx @@ -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, diff --git a/src/components/note/components/note-proxy-link.tsx b/src/components/note/timeline-note/components/note-proxy-link.tsx similarity index 85% rename from src/components/note/components/note-proxy-link.tsx rename to src/components/note/timeline-note/components/note-proxy-link.tsx index d976696fc..05d0fa64e 100644 --- a/src/components/note/components/note-proxy-link.tsx +++ b/src/components/note/timeline-note/components/note-proxy-link.tsx @@ -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, diff --git a/src/components/note/components/note-reactions.tsx b/src/components/note/timeline-note/components/note-reactions.tsx similarity index 70% rename from src/components/note/components/note-reactions.tsx rename to src/components/note/timeline-note/components/note-reactions.tsx index 799e29c77..85fe8238c 100644 --- a/src/components/note/components/note-reactions.tsx +++ b/src/components/note/timeline-note/components/note-reactions.tsx @@ -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 & { event: NostrEvent }) { const reactions = useEventReactions(event.id) ?? []; diff --git a/src/components/note/components/repost-button.tsx b/src/components/note/timeline-note/components/repost-button.tsx similarity index 80% rename from src/components/note/components/repost-button.tsx rename to src/components/note/timeline-note/components/repost-button.tsx index 60cf93e4e..2693c6035 100644 --- a/src/components/note/components/repost-button.tsx +++ b/src/components/note/timeline-note/components/repost-button.tsx @@ -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(); diff --git a/src/components/note/components/repost-modal.tsx b/src/components/note/timeline-note/components/repost-modal.tsx similarity index 84% rename from src/components/note/components/repost-modal.tsx rename to src/components/note/timeline-note/components/repost-modal.tsx index 63afa3ba6..2603786b3 100644 --- a/src/components/note/components/repost-modal.tsx +++ b/src/components/note/timeline-note/components/repost-modal.tsx @@ -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 ?? ""]); diff --git a/src/components/note/index.tsx b/src/components/note/timeline-note/index.tsx similarity index 78% rename from src/components/note/index.tsx rename to src/components/note/timeline-note/index.tsx index feb148895..87a15e3df 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/timeline-note/index.tsx @@ -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 & { 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); diff --git a/src/components/note/note-community-metadata.tsx b/src/components/note/timeline-note/note-community-metadata.tsx similarity index 83% rename from src/components/note/note-community-metadata.tsx rename to src/components/note/timeline-note/note-community-metadata.tsx index c57139285..64a845bca 100644 --- a/src/components/note/note-community-metadata.tsx +++ b/src/components/note/timeline-note/note-community-metadata.tsx @@ -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, diff --git a/src/components/note/note-content-with-warning.tsx b/src/components/note/timeline-note/note-content-with-warning.tsx similarity index 56% rename from src/components/note/note-content-with-warning.tsx rename to src/components/note/timeline-note/note-content-with-warning.tsx index d70a3c6d2..bcaedf4ea 100644 --- a/src/components/note/note-content-with-warning.tsx +++ b/src/components/note/timeline-note/note-content-with-warning.tsx @@ -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 ? ( ) : ( - + ); } diff --git a/src/components/note/text-note-contents.tsx b/src/components/note/timeline-note/text-note-contents.tsx similarity index 77% rename from src/components/note/text-note-contents.tsx rename to src/components/note/timeline-note/text-note-contents.tsx index a0a53c642..b8637d8b7 100644 --- a/src/components/note/text-note-contents.tsx +++ b/src/components/note/timeline-note/text-note-contents.tsx @@ -1,8 +1,8 @@ import React, { Suspense } from "react"; import { Box, BoxProps, Spinner } from "@chakra-ui/react"; +import { EventTemplate, NostrEvent } from "nostr-tools"; -import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; -import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../helpers/embeds"; +import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../../helpers/embeds"; import { embedLightningInvoice, embedNostrLinks, @@ -27,12 +27,12 @@ import { renderSimpleXLink, renderRedditUrl, embedNipDefinitions, -} from "../embed-types"; -import { LightboxProvider } from "../lightbox-provider"; -import { renderModelUrl } from "../embed-types/model"; -import { renderAudioUrl } from "../embed-types/audio"; + renderAudioUrl, + renderModelUrl, +} from "../../embed-types"; +import { LightboxProvider } from "../../lightbox-provider"; -function buildContents(event: NostrEvent | DraftNostrEvent, simpleLinks = false) { +function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) { let content: EmbedableContent = [event.content.trim()]; // image gallery @@ -74,14 +74,14 @@ function buildContents(event: NostrEvent | DraftNostrEvent, simpleLinks = false) return content; } -export type NoteContentsProps = { - event: NostrEvent | DraftNostrEvent; +export type TextNoteContentsProps = { + event: NostrEvent | EventTemplate; noOpenGraphLinks?: boolean; maxLength?: number; }; -export const NoteContents = React.memo( - ({ event, noOpenGraphLinks, maxLength, ...props }: NoteContentsProps & Omit) => { +export const TextNoteContents = React.memo( + ({ event, noOpenGraphLinks, maxLength, ...props }: TextNoteContentsProps & Omit) => { let content = buildContents(event, noOpenGraphLinks); if (maxLength !== undefined) { @@ -99,3 +99,5 @@ export const NoteContents = React.memo( ); }, ); + +export default TextNoteContents; diff --git a/src/components/npub-autocomplete.tsx b/src/components/npub-autocomplete.tsx index 6d829f99f..752e71256 100644 --- a/src/components/npub-autocomplete.tsx +++ b/src/components/npub-autocomplete.tsx @@ -5,7 +5,7 @@ import { nip19 } from "nostr-tools"; import { useUserSearchDirectoryContext } from "../providers/global/user-directory-provider"; import userMetadataService from "../services/user-metadata"; -import { getUserDisplayName } from "../helpers/user-metadata"; +import { getUserDisplayName } from "../helpers/nostr/user-metadata"; const NpubAutocomplete = forwardRef(({ value, ...props }, ref) => { const getDirectory = useUserSearchDirectoryContext(); diff --git a/src/components/open-graph-card.tsx b/src/components/open-graph/open-graph-card.tsx similarity index 90% rename from src/components/open-graph-card.tsx rename to src/components/open-graph/open-graph-card.tsx index dec2bf136..758c39c74 100644 --- a/src/components/open-graph-card.tsx +++ b/src/components/open-graph/open-graph-card.tsx @@ -11,8 +11,8 @@ import { LinkOverlay, Text, } from "@chakra-ui/react"; -import useOpenGraphData from "../hooks/use-open-graph-data"; -import { useBreakpointValue } from "../providers/global/breakpoint-provider"; +import useOpenGraphData from "../../hooks/use-open-graph-data"; +import { useBreakpointValue } from "../../providers/global/breakpoint-provider"; export default function OpenGraphCard({ url, ...props }: { url: URL } & Omit) { const { value: data } = useOpenGraphData(url); diff --git a/src/components/open-graph-link.tsx b/src/components/open-graph/open-graph-link.tsx similarity index 85% rename from src/components/open-graph-link.tsx rename to src/components/open-graph/open-graph-link.tsx index 7ce7868b1..b9da1e7aa 100644 --- a/src/components/open-graph-link.tsx +++ b/src/components/open-graph/open-graph-link.tsx @@ -1,5 +1,5 @@ import { Link, LinkProps } from "@chakra-ui/react"; -import useOpenGraphData from "../hooks/use-open-graph-data"; +import useOpenGraphData from "../../hooks/use-open-graph-data"; export default function OpenGraphLink({ url, ...props }: { url: URL } & Omit) { const { value: data } = useOpenGraphData(url); diff --git a/src/components/pagination-controls.tsx b/src/components/pagination-controls.tsx deleted file mode 100644 index 4137010f5..000000000 --- a/src/components/pagination-controls.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Button, ButtonGroup, ButtonProps, IconButton } from "@chakra-ui/react"; -import { useMemo } from "react"; -import { usePaginatedList } from "../hooks/use-paginated-list"; -import { ChevronLeftIcon, ChevronRightIcon } from "./icons"; - -const range = (start: number, end: number) => { - let length = end - start + 1; - return Array.from({ length }, (_, idx) => idx + start); -}; - -export type PaginationControlsProps = ReturnType & { - buttonSize?: ButtonProps["size"]; - siblingCount?: number; -}; - -export const PaginationControls = ({ - pageCount, - currentPage, - setPage, - next, - previous, - buttonSize, - siblingCount = 1, -}: PaginationControlsProps) => { - const renderPageButton = (pageNumber: number) => ( - - ); - - // copied from https://www.freecodecamp.org/news/build-a-custom-pagination-component-in-react/ - const renderPageButtons = () => { - // Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS - const totalPageNumbers = siblingCount + 5; - - /* - Case 1: - If the number of pages is less than the page numbers we want to show in our - paginationComponent, we return the range [1..pageCount] - */ - if (totalPageNumbers >= pageCount) { - return range(1, pageCount).map(renderPageButton); - } - - /* - Calculate left and right sibling index and make sure they are within range 1 and pageCount - */ - const leftSiblingIndex = Math.max(currentPage + 1 - siblingCount, 1); - const rightSiblingIndex = Math.min(currentPage + 1 + siblingCount, pageCount); - - /* - We do not show dots just when there is just one page number to be inserted between the extremes of sibling and the page limits i.e 1 and pageCount. Hence we are using leftSiblingIndex > 2 and rightSiblingIndex < pageCount - 2 - */ - const shouldShowLeftDots = leftSiblingIndex > 3; - const shouldShowRightDots = rightSiblingIndex < pageCount - 2; - - const firstPageIndex = 1; - const lastPageIndex = pageCount; - - /* - Case 2: No left dots to show, but rights dots to be shown - */ - if (!shouldShowLeftDots && shouldShowRightDots) { - let leftItemCount = 3 + 2 * siblingCount; - let leftRange = range(1, leftItemCount); - - return [ - ...leftRange.map(renderPageButton), - , - renderPageButton(lastPageIndex), - ]; - } - - /* - Case 3: No right dots to show, but left dots to be shown - */ - if (shouldShowLeftDots && !shouldShowRightDots) { - let rightItemCount = 3 + 2 * siblingCount; - let rightRange = range(pageCount - rightItemCount + 1, pageCount); - return [ - renderPageButton(firstPageIndex), - , - ...rightRange.map(renderPageButton), - ]; - } - - /* - Case 4: Both left and right dots to be shown - */ - if (shouldShowLeftDots && shouldShowRightDots) { - let middleRange = range(leftSiblingIndex, rightSiblingIndex); - return [ - renderPageButton(firstPageIndex), - , - ...middleRange.map(renderPageButton), - , - renderPageButton(lastPageIndex), - ]; - } - }; - - return ( - - } - aria-label="previous" - title="previous" - size={buttonSize} - onClick={previous} - isDisabled={currentPage === 0} - /> - {renderPageButtons()} - } - aria-label="next" - title="next" - size={buttonSize} - onClick={next} - isDisabled={currentPage === pageCount - 1} - /> - - ); -}; diff --git a/src/components/people-list-selection/people-list-selection.tsx b/src/components/people-list-selection/people-list-selection.tsx index d26c1b881..264780697 100644 --- a/src/components/people-list-selection/people-list-selection.tsx +++ b/src/components/people-list-selection/people-list-selection.tsx @@ -18,15 +18,15 @@ import { usePeopleListContext } from "../../providers/local/people-list-provider import useUserLists from "../../hooks/use-user-lists"; import useCurrentAccount from "../../hooks/use-current-account"; import { PEOPLE_LIST_KIND, getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; -import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID } from "../../helpers/nostr/event"; import useFavoriteLists from "../../hooks/use-favorite-lists"; import { NostrEvent } from "../../types/nostr-event"; import { useCallback, useState } from "react"; import useUserContactList from "../../hooks/use-user-contact-list"; import { useUserSearchDirectoryContext } from "../../providers/global/user-directory-provider"; import { matchSorter } from "match-sorter"; -import UserAvatar from "../user-avatar"; -import UserName from "../user-name"; +import UserAvatar from "../user/user-avatar"; +import UserName from "../user/user-name"; function ListCard({ list, ...props }: { list: NostrEvent } & Omit) { return ( diff --git a/src/components/post-modal/community-select.tsx b/src/components/post-modal/community-select.tsx index 442f822b7..44b729840 100644 --- a/src/components/post-modal/community-select.tsx +++ b/src/components/post-modal/community-select.tsx @@ -6,7 +6,7 @@ import useCurrentAccount from "../../hooks/use-current-account"; import { getCommunityName } from "../../helpers/nostr/communities"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; -import { getEventCoordinate } from "../../helpers/nostr/events"; +import { getEventCoordinate } from "../../helpers/nostr/event"; function CommunityOption({ pointer }: { pointer: AddressPointer }) { const community = useReplaceableEvent(pointer); diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx index 07a554524..7b6b4ad70 100644 --- a/src/components/post-modal/index.tsx +++ b/src/components/post-modal/index.tsx @@ -29,7 +29,6 @@ import { kinds } from "nostr-tools"; import { ChevronDownIcon, ChevronUpIcon, UploadImageIcon } from "../icons"; import NostrPublishAction from "../../classes/nostr-publish-action"; -import { NoteContents } from "../note/text-note-contents"; import { PublishDetails } from "../publish-details"; import { TrustProvider } from "../../providers/local/trust"; import { @@ -50,10 +49,11 @@ import useCurrentAccount from "../../hooks/use-current-account"; import useCacheForm from "../../hooks/use-cache-form"; import { useTextAreaUploadFileWithForm } from "../../hooks/use-textarea-upload-file"; import { useThrottle } from "react-use"; -import MinePOW from "../mine-pow"; +import MinePOW from "../pow/mine-pow"; import useAppSettings from "../../hooks/use-app-settings"; import { ErrorBoundary } from "../error-boundary"; import { usePublishEvent } from "../../providers/global/publish-provider"; +import { TextNoteContents } from "../note/timeline-note/text-note-contents"; type FormValues = { subject: string; @@ -199,7 +199,7 @@ export default function PostModal({ - + diff --git a/src/components/post-modal/zap-split-creator.tsx b/src/components/post-modal/zap-split-creator.tsx index 7bcff9c10..b10a95a8a 100644 --- a/src/components/post-modal/zap-split-creator.tsx +++ b/src/components/post-modal/zap-split-creator.tsx @@ -16,8 +16,8 @@ import { useForm } from "react-hook-form"; import { EventSplit } from "../../helpers/nostr/zaps"; import { AddIcon } from "../icons"; import { normalizeToHexPubkey } from "../../helpers/nip19"; -import UserAvatar from "../user-avatar"; -import UserLink from "../user-link"; +import UserAvatar from "../user/user-avatar"; +import UserLink from "../user/user-link"; import NpubAutocomplete from "../npub-autocomplete"; function getRemainingPercent(split: EventSplit) { diff --git a/src/components/mine-pow.tsx b/src/components/pow/mine-pow.tsx similarity index 96% rename from src/components/mine-pow.tsx rename to src/components/pow/mine-pow.tsx index d3206c3bf..35c462cb1 100644 --- a/src/components/mine-pow.tsx +++ b/src/components/pow/mine-pow.tsx @@ -2,8 +2,8 @@ import { useRef, useState } from "react"; import { Button, ButtonGroup, Flex, Heading, Progress, Text } from "@chakra-ui/react"; import { getEventHash, nip13 } from "nostr-tools"; -import { DraftNostrEvent } from "../types/nostr-event"; -import CheckCircle from "./icons/check-circle"; +import { DraftNostrEvent } from "../../types/nostr-event"; +import CheckCircle from "../icons/check-circle"; import { useMount } from "react-use"; const BATCH_NUMBER = 1000; diff --git a/src/components/pow-icon.tsx b/src/components/pow/pow-icon.tsx similarity index 66% rename from src/components/pow-icon.tsx rename to src/components/pow/pow-icon.tsx index 286081652..5e1353b6f 100644 --- a/src/components/pow-icon.tsx +++ b/src/components/pow/pow-icon.tsx @@ -1,12 +1,12 @@ import { IconProps, Tooltip } from "@chakra-ui/react"; -import { NostrEvent } from "../types/nostr-event"; +import { NostrEvent } from "../../types/nostr-event"; import { nip13 } from "nostr-tools"; -import Dice1 from "./icons/dice-1"; -import Dice2 from "./icons/dice-2"; -import Dice3 from "./icons/dice-3"; -import Dice4 from "./icons/dice-4"; -import Dice5 from "./icons/dice-5"; -import Dice6 from "./icons/dice-6"; +import Dice1 from "../icons/dice-1"; +import Dice2 from "../icons/dice-2"; +import Dice3 from "../icons/dice-3"; +import Dice4 from "../icons/dice-4"; +import Dice5 from "../icons/dice-5"; +import Dice6 from "../icons/dice-6"; export default function POWIcon({ event, ...props }: IconProps & { event: NostrEvent }) { const pow = nip13.getPow(event.id); diff --git a/src/components/publish-details.tsx b/src/components/publish-details.tsx index f5b03c204..abd22220b 100644 --- a/src/components/publish-details.tsx +++ b/src/components/publish-details.tsx @@ -17,17 +17,17 @@ export function PublishDetails({ pub }: PostResultsProps & Omit - {results.map((result) => ( - + {results.map(({ result, relay }) => ( + - - {result.relay.url} + + {relay.url} - + - {result.message && {result.message}} + {result[3] && {result[3]}} ))} diff --git a/src/components/publish-log.tsx b/src/components/publish-log.tsx index 6033c46c8..bc1417780 100644 --- a/src/components/publish-log.tsx +++ b/src/components/publish-log.tsx @@ -25,8 +25,8 @@ import { PublishContext } from "../providers/global/publish-provider"; export function PublishActionStatusTag({ pub, ...props }: { pub: NostrPublishAction } & Omit) { const results = useSubject(pub.results); - const successful = results.filter((result) => result.status); - const failedWithMessage = results.filter((result) => !result.status && result.message); + const successful = results.filter(({ result }) => result[2]); + const failedWithMessage = results.filter(({ result }) => !result[2] && result[3]); let statusIcon = ; let statusColor: TagProps["colorScheme"] = "blue"; diff --git a/src/components/qr-code-scanner-button.tsx b/src/components/qr-code/qr-code-scanner-button.tsx similarity index 94% rename from src/components/qr-code-scanner-button.tsx rename to src/components/qr-code/qr-code-scanner-button.tsx index 2ca961e07..8c1b65cb5 100644 --- a/src/components/qr-code-scanner-button.tsx +++ b/src/components/qr-code/qr-code-scanner-button.tsx @@ -1,6 +1,6 @@ import { IconButton, Spinner, useDisclosure } from "@chakra-ui/react"; import { type QrScannerModalProps } from "./qr-scanner-modal"; -import { QrCodeIcon } from "./icons"; +import { QrCodeIcon } from "../icons"; import { Suspense, lazy } from "react"; const QrScannerModal = lazy(() => import("./qr-scanner-modal")); diff --git a/src/components/qr-code-svg.tsx b/src/components/qr-code/qr-code-svg.tsx similarity index 86% rename from src/components/qr-code-svg.tsx rename to src/components/qr-code/qr-code-svg.tsx index 3151f3946..f063e9793 100644 --- a/src/components/qr-code-svg.tsx +++ b/src/components/qr-code/qr-code-svg.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import { drawSvgPath } from "../helpers/qrcode"; -import { Ecc, QrCode } from "../lib/qrcodegen"; +import { drawSvgPath } from "../../helpers/qrcode"; +import { Ecc, QrCode } from "../../lib/qrcodegen"; export default function QrCodeSvg({ content, diff --git a/src/components/qr-scanner-modal.tsx b/src/components/qr-code/qr-scanner-modal.tsx similarity index 100% rename from src/components/qr-scanner-modal.tsx rename to src/components/qr-code/qr-scanner-modal.tsx diff --git a/src/components/relay-list-button.tsx b/src/components/relay-list-button.tsx index 64464de34..08ee72b1a 100644 --- a/src/components/relay-list-button.tsx +++ b/src/components/relay-list-button.tsx @@ -14,7 +14,7 @@ import { import { isRTag } from "../types/nostr-event"; import useCurrentAccount from "../hooks/use-current-account"; import useUserRelaySets from "../hooks/use-user-relay-sets"; -import { getEventCoordinate } from "../helpers/nostr/events"; +import { getEventCoordinate } from "../helpers/nostr/event"; import { getListName } from "../helpers/nostr/lists"; import { relayListAddRelay, relayListRemoveRelay } from "../helpers/nostr/relay-list"; import { AddIcon, CheckIcon, ChevronDownIcon, InboxIcon, OutboxIcon, PlusCircleIcon } from "./icons"; diff --git a/src/components/relay-management-drawer/index.tsx b/src/components/relay-management-drawer/index.tsx index 52bac0484..b6e0bc6c1 100644 --- a/src/components/relay-management-drawer/index.tsx +++ b/src/components/relay-management-drawer/index.tsx @@ -30,7 +30,7 @@ import { RelayFavicon } from "../relay-favicon"; import useUserRelaySets from "../../hooks/use-user-relay-sets"; import useCurrentAccount from "../../hooks/use-current-account"; import { getListName } from "../../helpers/nostr/lists"; -import { getEventCoordinate } from "../../helpers/nostr/events"; +import { getEventCoordinate } from "../../helpers/nostr/event"; import AddRelayForm from "../../views/relays/app/add-relay-form"; import { SaveRelaySetForm } from "./save-relay-set-form"; diff --git a/src/components/relay-management-drawer/save-relay-set-form.tsx b/src/components/relay-management-drawer/save-relay-set-form.tsx index 64b51a001..d3dd5bb90 100644 --- a/src/components/relay-management-drawer/save-relay-set-form.tsx +++ b/src/components/relay-management-drawer/save-relay-set-form.tsx @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { getListDescription, getListName, setListDescription, setListName } from "../../helpers/nostr/lists"; import { isRTag } from "../../types/nostr-event"; -import { cloneEvent, ensureDTag } from "../../helpers/nostr/events"; +import { cloneEvent, ensureDTag } from "../../helpers/nostr/event"; import { createRTagsFromRelaySets } from "../../helpers/nostr/mailbox"; import { usePublishEvent } from "../../providers/global/publish-provider"; diff --git a/src/components/relay-selection/relay-selection-button.tsx b/src/components/relay-selection/relay-selection-button.tsx deleted file mode 100644 index 233f6d9a4..000000000 --- a/src/components/relay-selection/relay-selection-button.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ButtonProps, IconButton, useDisclosure } from "@chakra-ui/react"; - -import { RelayIcon } from "../icons"; -import RelayManagementDrawer from "../relay-management-drawer"; - -/** @deprecated */ -export default function RelaySelectionButton({ ...props }: ButtonProps) { - const relaysModal = useDisclosure(); - return ( - <> - } - onClick={relaysModal.onOpen} - aria-label="Relays" - title="Relays" - variant="ghost" - {...props} - /> - - - ); -} diff --git a/src/components/relay-selection/relay-selection-modal.tsx b/src/components/relay-selection/relay-selection-modal.tsx deleted file mode 100644 index 421e974b0..000000000 --- a/src/components/relay-selection/relay-selection-modal.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState } from "react"; -import { - Button, - ButtonGroup, - Checkbox, - CheckboxGroup, - Flex, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - useToast, -} from "@chakra-ui/react"; - -import { useReadRelays } from "../../hooks/use-client-relays"; -import { RelayFavicon } from "../relay-favicon"; -import { RelayUrlInput } from "../relay-url-input"; -import { unique } from "../../helpers/array"; -import relayScoreboardService from "../../services/relay-scoreboard"; -import { normalizeRelayURL } from "../../helpers/relay"; - -function AddRelayForm({ onSubmit }: { onSubmit: (relay: string) => void }) { - const [url, setUrl] = useState(""); - const toast = useToast(); - - return ( - { - try { - e.preventDefault(); - onSubmit(normalizeRelayURL(url)); - setUrl(""); - } catch (err) { - if (err instanceof Error) { - toast({ status: "error", description: err.message }); - } - } - }} - gap="2" - mb="4" - > - setUrl(e.target.value)} /> - - - ); -} - -const manuallyAddedRelays = new Set(); - -export default function RelaySelectionModal({ - selected, - onClose, - onSubmit, -}: { - selected: string[]; - onSubmit: (relays: string[]) => void; - onClose: () => void; -}) { - const [newSelected, setSelected] = useState(selected); - const relays = useReadRelays([...selected, ...newSelected, ...Array.from(manuallyAddedRelays)]); - - return ( - - - - Select Relays - - - { - setSelected(unique([newRelay, ...newSelected])); - manuallyAddedRelays.add(newRelay); - }} - /> - setSelected(urls.map(String))}> - - {relays.urls.map((url) => ( - - {url} - - ))} - - - - - - - - - - - - - - - - ); -} diff --git a/src/components/back-button.tsx b/src/components/router/back-button.tsx similarity index 90% rename from src/components/back-button.tsx rename to src/components/router/back-button.tsx index bad0dad71..9155e1b43 100644 --- a/src/components/back-button.tsx +++ b/src/components/router/back-button.tsx @@ -1,5 +1,5 @@ import { IconButton, IconButtonProps } from "@chakra-ui/react"; -import { ChevronLeftIcon } from "./icons"; +import { ChevronLeftIcon } from "../icons"; import { useNavigate } from "react-router-dom"; export default function BackButton({ ...props }: Omit) { diff --git a/src/components/search-modal/index.tsx b/src/components/search-modal/index.tsx index 0f842b64e..da0759c5b 100644 --- a/src/components/search-modal/index.tsx +++ b/src/components/search-modal/index.tsx @@ -6,9 +6,9 @@ import { matchSorter } from "match-sorter"; import { nip19 } from "nostr-tools"; import { useUserSearchDirectoryContext } from "../../providers/global/user-directory-provider"; -import UserAvatar from "../user-avatar"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; -import { getUserDisplayName } from "../../helpers/user-metadata"; +import UserAvatar from "../user/user-avatar"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../helpers/nostr/user-metadata"; function UserOption({ pubkey }: { pubkey: string }) { const metadata = useUserMetadata(pubkey); diff --git a/src/components/setup/index.tsx b/src/components/setup/index.tsx deleted file mode 100644 index 578d42b76..000000000 --- a/src/components/setup/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from "react"; -import { - Button, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - useDisclosure, -} from "@chakra-ui/react"; -import { useReadRelays } from "../../hooks/use-client-relays"; - -export default function Setup() { - const relaysModal = useDisclosure(); - - const readRelays = useReadRelays(); - useEffect(() => (readRelays.size === 0 ? relaysModal.onOpen() : relaysModal.onClose()), [readRelays]); - - return ( - <> - - - - Setup Relays - - - - - - - - - - - ); -} diff --git a/src/components/timeline-page/generic-note-timeline/article-note.tsx b/src/components/timeline-page/generic-note-timeline/article-note.tsx index 6f981cf03..151a9944b 100644 --- a/src/components/timeline-page/generic-note-timeline/article-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/article-note.tsx @@ -4,7 +4,7 @@ import { Box } from "@chakra-ui/react"; import { NostrEvent } from "../../../types/nostr-event"; import EmbeddedArticle from "../../embed-event/event-types/embedded-article"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; function ArticleNote({ article }: { article: NostrEvent }) { const ref = useRef(null); diff --git a/src/components/timeline-page/generic-note-timeline/index.tsx b/src/components/timeline-page/generic-note-timeline/index.tsx index d76b3de78..1210ce910 100644 --- a/src/components/timeline-page/generic-note-timeline/index.tsx +++ b/src/components/timeline-page/generic-note-timeline/index.tsx @@ -6,7 +6,7 @@ import { useLocation } from "react-router-dom"; import useSubject from "../../../hooks/use-subject"; import TimelineLoader from "../../../classes/timeline-loader"; import { NostrEvent } from "../../../types/nostr-event"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { ExtendedIntersectionObserverEntry, useIntersectionObserver, @@ -132,9 +132,9 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) { } }; - intersectionSubject.subscribe(listener); + const sub = intersectionSubject.subscribe(listener); return () => { - intersectionSubject.unsubscribe(listener); + sub.unsubscribe(); }; }, [ setPinDate, diff --git a/src/components/timeline-page/generic-note-timeline/relay-recommendation.tsx b/src/components/timeline-page/generic-note-timeline/relay-recommendation.tsx index 6787b269d..5b73027da 100644 --- a/src/components/timeline-page/generic-note-timeline/relay-recommendation.tsx +++ b/src/components/timeline-page/generic-note-timeline/relay-recommendation.tsx @@ -2,8 +2,8 @@ import { useRef } from "react"; import { Flex, Text } from "@chakra-ui/react"; import { NostrEvent } from "../../../types/nostr-event"; -import UserAvatar from "../../user-avatar"; -import UserLink from "../../user-link"; +import UserAvatar from "../../user/user-avatar"; +import UserLink from "../../user/user-link"; import RelayCard from "../../../views/relays/components/relay-card"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import { safeRelayUrl } from "../../../helpers/relay"; diff --git a/src/components/timeline-page/generic-note-timeline/reply-note.tsx b/src/components/timeline-page/generic-note-timeline/reply-note.tsx index fe5fb9957..353bd2120 100644 --- a/src/components/timeline-page/generic-note-timeline/reply-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/reply-note.tsx @@ -2,7 +2,7 @@ import { memo, useRef } from "react"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import Note from "../../note"; +import TimelineNote from "../../note/timeline-note"; import { getEventUID } from "nostr-idb"; function ReplyNote({ event }: { event: NostrEvent }) { @@ -11,7 +11,7 @@ function ReplyNote({ event }: { event: NostrEvent }) { return (
- +
); } diff --git a/src/components/timeline-page/generic-note-timeline/repost-event.tsx b/src/components/timeline-page/generic-note-timeline/repost-event.tsx index 103d202cc..abfc74bbb 100644 --- a/src/components/timeline-page/generic-note-timeline/repost-event.tsx +++ b/src/components/timeline-page/generic-note-timeline/repost-event.tsx @@ -4,19 +4,19 @@ import { kinds, nip18 } from "nostr-tools"; import { Link as RouterLink } from "react-router-dom"; import { NostrEvent } from "../../../types/nostr-event"; -import { Note } from "../../note"; -import NoteMenu from "../../note/note-menu"; -import UserAvatar from "../../user-avatar"; -import { UserDnsIdentityIcon } from "../../user-dns-identity-icon"; -import UserLink from "../../user-link"; +import TimelineNote from "../../note/timeline-note"; +import UserAvatar from "../../user/user-avatar"; +import { UserDnsIdentityIcon } from "../../user/user-dns-identity-icon"; +import UserLink from "../../user/user-link"; import { TrustProvider } from "../../../providers/local/trust"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import useSingleEvent from "../../../hooks/use-single-event"; import { EmbedEvent } from "../../embed-event"; import useUserMuteFilter from "../../../hooks/use-user-mute-filter"; -import { parseHardcodedNoteContent } from "../../../helpers/nostr/events"; +import { parseHardcodedNoteContent } from "../../../helpers/nostr/event"; import { getEventCommunityPointer } from "../../../helpers/nostr/communities"; import LoadingNostrLink from "../../loading-nostr-link"; +import NoteMenu from "../../note/note-menu"; function RepostEvent({ event }: { event: NostrEvent }) { const muteFilter = useUserMuteFilter(); @@ -60,7 +60,7 @@ function RepostEvent({ event }: { event: NostrEvent }) { ) : note.kind === kinds.ShortTextNote ? ( // NOTE: tell the note not to register itself with the intersection observer. since this is an older note it will break the order of the timeline - + ) : ( )} diff --git a/src/components/timeline-page/generic-note-timeline/stream-note.tsx b/src/components/timeline-page/generic-note-timeline/stream-note.tsx index 35beca7f9..c4c356eba 100644 --- a/src/components/timeline-page/generic-note-timeline/stream-note.tsx +++ b/src/components/timeline-page/generic-note-timeline/stream-note.tsx @@ -20,13 +20,13 @@ import { NostrEvent } from "../../../types/nostr-event"; import { parseStreamEvent } from "../../../helpers/nostr/stream"; import useEventNaddr from "../../../hooks/use-event-naddr"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import UserAvatar from "../../user-avatar"; -import UserLink from "../../user-link"; +import UserAvatar from "../../user/user-avatar"; +import UserLink from "../../user/user-link"; import StreamStatusBadge from "../../../views/streams/components/status-badge"; -import { EventRelays } from "../../note/note-relays"; import { useAsync } from "react-use"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import Timestamp from "../../timestamp"; +import { EventRelays } from "../../note/event-relays"; export default function StreamNote({ event, ...props }: CardProps & { event: NostrEvent }) { const { value: stream, error } = useAsync(async () => parseStreamEvent(event), [event]); diff --git a/src/components/timeline-page/generic-note-timeline/timeline-item.tsx b/src/components/timeline-page/generic-note-timeline/timeline-item.tsx index 8845194ea..e52010d7a 100644 --- a/src/components/timeline-page/generic-note-timeline/timeline-item.tsx +++ b/src/components/timeline-page/generic-note-timeline/timeline-item.tsx @@ -4,18 +4,18 @@ import { Box, Text } from "@chakra-ui/react"; import { ErrorBoundary } from "../../error-boundary"; import ReplyNote from "./reply-note"; -import Note from "../../note"; import RepostEvent from "./repost-event"; import ArticleNote from "./article-note"; import StreamNote from "./stream-note"; import RelayRecommendation from "./relay-recommendation"; import BadgeAwardCard from "../../../views/badges/components/badge-award-card"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID, isReply } from "../../../helpers/nostr/events"; +import { getEventUID, isReply } from "../../../helpers/nostr/event"; import { STREAM_KIND } from "../../../helpers/nostr/stream"; import { NostrEvent } from "../../../types/nostr-event"; import { FLARE_VIDEO_KIND } from "../../../helpers/nostr/flare"; import EmbeddedFlareVideo from "../../embed-event/event-types/embedded-flare-video"; +import { TimelineNote } from "../../note/timeline-note"; function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visible: boolean; minHeight?: number }) { const ref = useRef(null); @@ -24,7 +24,7 @@ function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visibl let content: ReactNode | null = null; switch (event.kind) { case kinds.ShortTextNote: - content = isReply(event) ? : ; + content = isReply(event) ? : ; break; case kinds.Repost: case kinds.GenericRepost: diff --git a/src/components/timeline-page/media-timeline/index.tsx b/src/components/timeline-page/media-timeline/index.tsx index 3bca48586..84b65d93b 100644 --- a/src/components/timeline-page/media-timeline/index.tsx +++ b/src/components/timeline-page/media-timeline/index.tsx @@ -12,7 +12,7 @@ import { TrustProvider } from "../../../providers/local/trust"; import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import { NostrEvent } from "../../../types/nostr-event"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { useBreakpointValue } from "../../../providers/global/breakpoint-provider"; function CustomGalleryImage({ event, ...props }: EmbeddedImageProps & { event: NostrEvent }) { diff --git a/src/components/timeline-page/timeline-action-and-status.tsx b/src/components/timeline-page/timeline-action-and-status.tsx index caf18110b..81fef51a3 100644 --- a/src/components/timeline-page/timeline-action-and-status.tsx +++ b/src/components/timeline-page/timeline-action-and-status.tsx @@ -21,7 +21,14 @@ export default function TimelineActionAndStatus({ timeline }: { timeline: Timeli } return ( - ); diff --git a/src/components/timeline-page/timeline-health/index.tsx b/src/components/timeline-page/timeline-health/index.tsx index a2e1bdf98..81f8c078e 100644 --- a/src/components/timeline-page/timeline-health/index.tsx +++ b/src/components/timeline-page/timeline-health/index.tsx @@ -21,9 +21,9 @@ import { getEventRelays } from "../../../services/event-relays"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import { RelayFavicon } from "../../relay-favicon"; -import { NoteLink } from "../../note-link"; +import { NoteLink } from "../../note/note-link"; import { BroadcastEventIcon } from "../../icons"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import Timestamp from "../../timestamp"; import { usePublishEvent } from "../../../providers/global/publish-provider"; diff --git a/src/components/user-avatar-link.tsx b/src/components/user/user-avatar-link.tsx similarity index 100% rename from src/components/user-avatar-link.tsx rename to src/components/user/user-avatar-link.tsx diff --git a/src/components/user-avatar.tsx b/src/components/user/user-avatar.tsx similarity index 83% rename from src/components/user-avatar.tsx rename to src/components/user/user-avatar.tsx index 9beb1c08b..38888f9c3 100644 --- a/src/components/user-avatar.tsx +++ b/src/components/user/user-avatar.tsx @@ -2,13 +2,13 @@ import { forwardRef, memo, useMemo } from "react"; import { Avatar, AvatarProps } from "@chakra-ui/react"; import { useAsync } from "react-use"; -import { useUserMetadata } from "../hooks/use-user-metadata"; -import { getIdenticon } from "../helpers/identicon"; -import { safeUrl } from "../helpers/parse"; -import { Kind0ParsedContent, getUserDisplayName } from "../helpers/user-metadata"; -import useAppSettings from "../hooks/use-app-settings"; -import useCurrentAccount from "../hooks/use-current-account"; -import { buildImageProxyURL } from "../helpers/image"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import { getIdenticon } from "../../helpers/identicon"; +import { safeUrl } from "../../helpers/parse"; +import { Kind0ParsedContent, getUserDisplayName } from "../../helpers/nostr/user-metadata"; +import useAppSettings from "../../hooks/use-app-settings"; +import useCurrentAccount from "../../hooks/use-current-account"; +import { buildImageProxyURL } from "../../helpers/image"; export const UserIdenticon = memo(({ pubkey }: { pubkey: string }) => { const { value: identicon } = useAsync(() => getIdenticon(pubkey), [pubkey]); diff --git a/src/components/user-dns-identity-icon.tsx b/src/components/user/user-dns-identity-icon.tsx similarity index 76% rename from src/components/user-dns-identity-icon.tsx rename to src/components/user/user-dns-identity-icon.tsx index e5ee5fd10..73bd5968f 100644 --- a/src/components/user-dns-identity-icon.tsx +++ b/src/components/user/user-dns-identity-icon.tsx @@ -1,10 +1,10 @@ import { Text, Tooltip } from "@chakra-ui/react"; -import { useDnsIdentity } from "../hooks/use-dns-identity"; -import { useUserMetadata } from "../hooks/use-user-metadata"; -import { VerificationFailed, VerificationMissing, VerifiedIcon } from "./icons"; +import useDnsIdentity from "../../hooks/use-dns-identity"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import { VerificationFailed, VerificationMissing, VerifiedIcon } from "../icons"; -export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) => { +export function UserDnsIdentityIcon({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) { const metadata = useUserMetadata(pubkey); const identity = useDnsIdentity(metadata?.nip05); @@ -32,4 +32,6 @@ export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; only {metadata.nip05.startsWith("_@") ? metadata.nip05.substr(2) : metadata.nip05} {renderIcon()} ); -}; +} + +export default UserDnsIdentityIcon; diff --git a/src/components/user-follow-button.tsx b/src/components/user/user-follow-button.tsx similarity index 88% rename from src/components/user-follow-button.tsx rename to src/components/user/user-follow-button.tsx index ef1f890d1..f6b972a1d 100644 --- a/src/components/user-follow-button.tsx +++ b/src/components/user/user-follow-button.tsx @@ -12,9 +12,9 @@ import { useDisclosure, } from "@chakra-ui/react"; -import useCurrentAccount from "../hooks/use-current-account"; -import { ChevronDownIcon, FollowIcon, MuteIcon, PlusCircleIcon, UnfollowIcon, UnmuteIcon } from "./icons"; -import useUserLists from "../hooks/use-user-lists"; +import useCurrentAccount from "../../hooks/use-current-account"; +import { ChevronDownIcon, FollowIcon, MuteIcon, PlusCircleIcon, UnfollowIcon, UnmuteIcon } from "../icons"; +import useUserLists from "../../hooks/use-user-lists"; import { PEOPLE_LIST_KIND, createEmptyContactList, @@ -23,15 +23,15 @@ import { getListName, getPubkeysFromList, isPubkeyInList, -} from "../helpers/nostr/lists"; -import { getEventCoordinate } from "../helpers/nostr/events"; -import { useSigningContext } from "../providers/global/signing-provider"; -import useUserContactList from "../hooks/use-user-contact-list"; -import useAsyncErrorHandler from "../hooks/use-async-error-handler"; -import NewListModal from "../views/lists/components/new-list-modal"; -import useUserMuteActions from "../hooks/use-user-mute-actions"; -import { useMuteModalContext } from "../providers/route/mute-modal-provider"; -import { usePublishEvent } from "../providers/global/publish-provider"; +} from "../../helpers/nostr/lists"; +import { getEventCoordinate } from "../../helpers/nostr/event"; +import { useSigningContext } from "../../providers/global/signing-provider"; +import useUserContactList from "../../hooks/use-user-contact-list"; +import useAsyncErrorHandler from "../../hooks/use-async-error-handler"; +import NewListModal from "../../views/lists/components/new-list-modal"; +import useUserMuteActions from "../../hooks/use-user-mute-actions"; +import { useMuteModalContext } from "../../providers/route/mute-modal-provider"; +import { usePublishEvent } from "../../providers/global/publish-provider"; function UsersLists({ pubkey }: { pubkey: string }) { const publish = usePublishEvent(); diff --git a/src/components/user-link.tsx b/src/components/user/user-link.tsx similarity index 74% rename from src/components/user-link.tsx rename to src/components/user/user-link.tsx index 0efdeb358..43017fb82 100644 --- a/src/components/user-link.tsx +++ b/src/components/user/user-link.tsx @@ -2,10 +2,10 @@ import { Link, LinkProps } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import { getUserDisplayName } from "../helpers/user-metadata"; -import { useUserMetadata } from "../hooks/use-user-metadata"; -import useAppSettings from "../hooks/use-app-settings"; -import useCurrentAccount from "../hooks/use-current-account"; +import { getUserDisplayName } from "../../helpers/nostr/user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import useAppSettings from "../../hooks/use-app-settings"; +import useCurrentAccount from "../../hooks/use-current-account"; export type UserLinkProps = LinkProps & { pubkey: string; diff --git a/src/components/user-name.tsx b/src/components/user/user-name.tsx similarity index 68% rename from src/components/user-name.tsx rename to src/components/user/user-name.tsx index ad4dfc483..26d4cc3e4 100644 --- a/src/components/user-name.tsx +++ b/src/components/user/user-name.tsx @@ -1,8 +1,8 @@ import { Text, TextProps } from "@chakra-ui/react"; -import { getUserDisplayName } from "../helpers/user-metadata"; -import { useUserMetadata } from "../hooks/use-user-metadata"; -import useAppSettings from "../hooks/use-app-settings"; +import { getUserDisplayName } from "../../helpers/nostr/user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import useAppSettings from "../../hooks/use-app-settings"; export default function UserName({ pubkey, ...props }: Omit & { pubkey: string }) { const metadata = useUserMetadata(pubkey); diff --git a/src/helpers/nip19.ts b/src/helpers/nip19.ts index d2bfaa2f0..61909ab97 100644 --- a/src/helpers/nip19.ts +++ b/src/helpers/nip19.ts @@ -1,7 +1,7 @@ import { getPublicKey, nip19 } from "nostr-tools"; import { NostrEvent, Tag, isATag, isDTag, isETag, isPTag } from "../types/nostr-event"; -import { isReplaceable } from "./nostr/events"; +import { isReplaceable } from "./nostr/event"; import relayHintService from "../services/event-relay-hint"; import { safeRelayUrls } from "./relay"; diff --git a/src/helpers/nostr/communities.ts b/src/helpers/nostr/communities.ts index c581099cc..2a7518ff0 100644 --- a/src/helpers/nostr/communities.ts +++ b/src/helpers/nostr/communities.ts @@ -2,7 +2,7 @@ import { kinds, validateEvent } from "nostr-tools"; import { NostrEvent, isATag, isDTag, isETag, isPTag } from "../../types/nostr-event"; import { getMatchLink, getMatchNostrLink } from "../regexp"; import { ReactionGroup } from "./reactions"; -import { parseCoordinate } from "./events"; +import { parseCoordinate } from "./event"; /** @deprecated */ export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities"; diff --git a/src/helpers/nostr/dms.ts b/src/helpers/nostr/dms.ts index 4a6eb52c7..7b444a6d1 100644 --- a/src/helpers/nostr/dms.ts +++ b/src/helpers/nostr/dms.ts @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { NostrEvent, isPTag } from "../../types/nostr-event"; -import { sortByDate } from "./events"; +import { sortByDate } from "./event"; export function getDMSender(event: NostrEvent) { return event.pubkey; diff --git a/src/helpers/nostr/events.ts b/src/helpers/nostr/event.ts similarity index 100% rename from src/helpers/nostr/events.ts rename to src/helpers/nostr/event.ts index 466a7fa2b..c9decd7be 100644 --- a/src/helpers/nostr/events.ts +++ b/src/helpers/nostr/event.ts @@ -1,14 +1,14 @@ import { EventTemplate, kinds, validateEvent } from "nostr-tools"; +import { getEventUID } from "nostr-idb"; +import dayjs from "dayjs"; +import { nanoid } from "nanoid"; import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, isPTag, NostrEvent, Tag } from "../../types/nostr-event"; import { getMatchNostrLink } from "../regexp"; import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19"; import { safeJson } from "../parse"; import { safeDecode } from "../nip19"; -import { getEventUID } from "nostr-idb"; import { safeRelayUrl, safeRelayUrls } from "../relay"; -import dayjs from "dayjs"; -import { nanoid } from "nanoid"; import userMailboxesService from "../../services/user-mailboxes"; import RelaySet from "../../classes/relay-set"; diff --git a/src/helpers/nostr/filter.ts b/src/helpers/nostr/filter.ts index 97277b99a..38cffd7ea 100644 --- a/src/helpers/nostr/filter.ts +++ b/src/helpers/nostr/filter.ts @@ -1,19 +1,21 @@ import stringify from "json-stringify-deterministic"; -import { NostrRequestFilter, RelayQueryMap } from "../../types/nostr-query"; +import { RelayQueryMap } from "../../types/nostr-relay"; import { Filter } from "nostr-tools"; import { safeRelayUrls } from "../relay"; -export function addQueryToFilter(filter: NostrRequestFilter, query: Filter) { +export function mergeFilter(filter: Filter, query: Filter): Filter; +export function mergeFilter(filter: Filter[], query: Filter): Filter[]; +export function mergeFilter(filter: Filter | Filter[], query: Filter) { if (Array.isArray(filter)) { return filter.map((f) => ({ ...f, ...query })); } return { ...filter, ...query }; } -export function stringifyFilter(filter: NostrRequestFilter) { +export function stringifyFilter(filter: Filter | Filter[]) { return stringify(filter); } -export function isFilterEqual(a: NostrRequestFilter, b: NostrRequestFilter) { +export function isFilterEqual(a: Filter | Filter[], b: Filter | Filter[]) { return stringifyFilter(a) === stringifyFilter(b); } @@ -21,14 +23,14 @@ export function isQueryMapEqual(a: RelayQueryMap, b: RelayQueryMap) { return stringify(a) === stringify(b); } -export function mapQueryMap(queryMap: RelayQueryMap, fn: (filter: NostrRequestFilter) => NostrRequestFilter) { +export function mapQueryMap(queryMap: RelayQueryMap, fn: (filters: Filter[]) => Filter[]) { const newMap: RelayQueryMap = {}; - for (const [relay, filter] of Object.entries(queryMap)) newMap[relay] = fn(filter); + for (const [relay, filters] of Object.entries(queryMap)) newMap[relay] = fn(filters); return newMap; } -export function createSimpleQueryMap(relays: Iterable, filter: NostrRequestFilter) { +export function createSimpleQueryMap(relays: Iterable, filters: Filter | Filter[]) { const map: RelayQueryMap = {}; - for (const relay of safeRelayUrls(relays)) map[relay] = filter; + for (const relay of safeRelayUrls(relays)) map[relay] = Array.isArray(filters) ? filters : [filters]; return map; } diff --git a/src/helpers/nostr/lists.ts b/src/helpers/nostr/lists.ts index 64ceb17b9..1f47822c6 100644 --- a/src/helpers/nostr/lists.ts +++ b/src/helpers/nostr/lists.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import { kinds, nip19 } from "nostr-tools"; import { DraftNostrEvent, NostrEvent, PTag, isATag, isDTag, isETag, isPTag, isRTag } from "../../types/nostr-event"; -import { parseCoordinate, replaceOrAddSimpleTag } from "./events"; +import { parseCoordinate, replaceOrAddSimpleTag } from "./event"; import { getRelayVariations, safeRelayUrls } from "../relay"; export const MUTE_LIST_KIND = 10000; diff --git a/src/helpers/nostr/mailbox.ts b/src/helpers/nostr/mailbox.ts index 4846d3d65..6c3e2aac0 100644 --- a/src/helpers/nostr/mailbox.ts +++ b/src/helpers/nostr/mailbox.ts @@ -2,7 +2,7 @@ import { kinds } from "nostr-tools"; import { RelayMode } from "../../classes/relay"; import { DraftNostrEvent, NostrEvent, RTag, Tag, isRTag } from "../../types/nostr-event"; import { safeRelayUrl } from "../relay"; -import { cloneEvent } from "./events"; +import { cloneEvent } from "./event"; /** fixes or removes any bad r tags */ export function cleanRTags(tags: Tag[]) { diff --git a/src/helpers/nostr/post.ts b/src/helpers/nostr/post.ts index b89bf23ef..ccecc643c 100644 --- a/src/helpers/nostr/post.ts +++ b/src/helpers/nostr/post.ts @@ -1,6 +1,6 @@ import { DraftNostrEvent, NostrEvent, Tag, isETag, isPTag } from "../../types/nostr-event"; import { getMatchEmoji, getMatchHashtag, getMatchNostrLink } from "../regexp"; -import { addPubkeyRelayHints, getThreadReferences } from "./events"; +import { addPubkeyRelayHints, getThreadReferences } from "./event"; import { getPubkeyFromDecodeResult, safeDecode } from "../nip19"; import { Emoji } from "../../providers/global/emoji-provider"; import { EventSplit } from "./zaps"; diff --git a/src/helpers/nostr/reactions.ts b/src/helpers/nostr/reactions.ts index 9807c6df9..aee5de3a3 100644 --- a/src/helpers/nostr/reactions.ts +++ b/src/helpers/nostr/reactions.ts @@ -1,7 +1,7 @@ import { kinds } from "nostr-tools"; import { DraftNostrEvent, NostrEvent, Tag } from "../../types/nostr-event"; import dayjs from "dayjs"; -import { getEventCoordinate, isReplaceable } from "./events"; +import { getEventCoordinate, isReplaceable } from "./event"; export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] }; @@ -26,9 +26,13 @@ export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) ["e", event.id], ["p", event.pubkey], ]; + + let content = emoji; + if (url && !content.startsWith(":") && content.endsWith(":")) content = ":" + content + ":"; + const draft: DraftNostrEvent = { kind: kinds.Reaction, - content: url ? ":" + emoji + ":" : emoji, + content, tags: isReplaceable(event.kind) ? [...tags, ["a", getEventCoordinate(event)]] : tags, created_at: dayjs().unix(), }; diff --git a/src/helpers/nostr/stream.ts b/src/helpers/nostr/stream.ts index 3e0fe91be..5a732d9b3 100644 --- a/src/helpers/nostr/stream.ts +++ b/src/helpers/nostr/stream.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event"; import { unique } from "../array"; import { ensureNotifyContentMentions } from "./post"; -import { createCoordinate } from "../../services/replaceable-event-requester"; +import { getEventCoordinate } from "./event"; export const STREAM_KIND = 30311; export const STREAM_CHAT_MESSAGE_KIND = 1311; @@ -87,7 +87,7 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream { } export function getATag(stream: ParsedStream) { - return createCoordinate(stream.event.kind, stream.author, stream.identifier); + return getEventCoordinate(stream.event); } export function buildChatMessage(stream: ParsedStream, content: string) { diff --git a/src/helpers/user-metadata.ts b/src/helpers/nostr/user-metadata.ts similarity index 82% rename from src/helpers/user-metadata.ts rename to src/helpers/nostr/user-metadata.ts index 28b6328d3..a45960bff 100644 --- a/src/helpers/user-metadata.ts +++ b/src/helpers/nostr/user-metadata.ts @@ -1,6 +1,5 @@ -import { nip19 } from "nostr-tools"; -import { NostrEvent } from "../types/nostr-event"; -import { truncatedId } from "./nostr/events"; +import { NostrEvent, nip19 } from "nostr-tools"; +import { truncatedId } from "./event"; export type Kind0ParsedContent = { pubkey?: string; @@ -18,8 +17,7 @@ export type Kind0ParsedContent = { nip05?: string; }; -export function parseKind0Event(event: NostrEvent): Kind0ParsedContent { - if (event.kind !== 0) throw new Error("expected a kind 0 event"); +export function parseMetadataContent(event: NostrEvent): Kind0ParsedContent { try { const metadata = JSON.parse(event.content) as Kind0ParsedContent; metadata.pubkey = event.pubkey; diff --git a/src/helpers/nostr/zaps.ts b/src/helpers/nostr/zaps.ts index 96ba7861d..48e497c22 100644 --- a/src/helpers/nostr/zaps.ts +++ b/src/helpers/nostr/zaps.ts @@ -2,7 +2,7 @@ import { bech32 } from "@scure/base"; import { isETag, isPTag, NostrEvent } from "../../types/nostr-event"; import { ParsedInvoice, parsePaymentRequest } from "../bolt11"; -import { Kind0ParsedContent } from "../user-metadata"; +import { Kind0ParsedContent } from "./user-metadata"; import { utils } from "nostr-tools"; // based on https://github.com/nbd-wtf/nostr-tools/blob/master/nip57.ts diff --git a/src/helpers/notification.ts b/src/helpers/notification.ts index 64cc5a931..936023e5d 100644 --- a/src/helpers/notification.ts +++ b/src/helpers/notification.ts @@ -1,6 +1,6 @@ import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; -import { getThreadReferences, sortByDate } from "./nostr/events"; +import { getThreadReferences, sortByDate } from "./nostr/event"; const DAY_IN_SECONDS = 60 * 60 * 24; diff --git a/src/helpers/relay.ts b/src/helpers/relay.ts index 43d70af21..31a334d2e 100644 --- a/src/helpers/relay.ts +++ b/src/helpers/relay.ts @@ -1,7 +1,7 @@ import { SimpleRelay, SubscriptionOptions } from "nostr-idb"; import { Filter } from "nostr-tools"; -import { NostrQuery, NostrRequestFilter } from "../types/nostr-query"; +import { NostrQuery, NostrRequestFilter } from "../types/nostr-relay"; import { NostrEvent } from "../types/nostr-event"; // NOTE: only use this for equality checks and querying diff --git a/src/helpers/thread.ts b/src/helpers/thread.ts index e6b1f4e69..51cbfc8e7 100644 --- a/src/helpers/thread.ts +++ b/src/helpers/thread.ts @@ -1,5 +1,5 @@ import { NostrEvent } from "../types/nostr-event"; -import { EventReferences, getThreadReferences } from "./nostr/events"; +import { EventReferences, getThreadReferences } from "./nostr/event"; export function countReplies(replies: ThreadItem[]): number { return replies.reduce((c, item) => c + countReplies(item.replies), 0) + replies.length; diff --git a/src/hooks/use-channel-metadata.ts b/src/hooks/use-channel-metadata.ts index 0c1285f37..9ac304c5e 100644 --- a/src/hooks/use-channel-metadata.ts +++ b/src/hooks/use-channel-metadata.ts @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import useSubject from "./use-subject"; import channelMetadataService from "../services/channel-metadata"; import { ChannelMetadata, safeParseChannelMetadata } from "../helpers/nostr/channel"; diff --git a/src/hooks/use-count-community-members.ts b/src/hooks/use-count-community-members.ts index 1ab222a42..9c7262e28 100644 --- a/src/hooks/use-count-community-members.ts +++ b/src/hooks/use-count-community-members.ts @@ -1,4 +1,4 @@ -import { getEventCoordinate } from "../helpers/nostr/events"; +import { getEventCoordinate } from "../helpers/nostr/event"; import { COMMUNITIES_LIST_KIND } from "../helpers/nostr/lists"; import { NostrEvent } from "../types/nostr-event"; import useEventCount from "./use-event-count"; diff --git a/src/hooks/use-dns-identity.ts b/src/hooks/use-dns-identity.ts index 114eab65e..b8672a9cc 100644 --- a/src/hooks/use-dns-identity.ts +++ b/src/hooks/use-dns-identity.ts @@ -1,8 +1,8 @@ -import dnsIdentityService from "../services/dns-identity"; import { useMemo } from "react"; +import dnsIdentityService from "../services/dns-identity"; import useSubject from "./use-subject"; -export function useDnsIdentity(address: string | undefined) { +export default function useDnsIdentity(address: string | undefined) { const subject = useMemo(() => { if (address) return dnsIdentityService.getIdentity(address); }, [address]); diff --git a/src/hooks/use-element-blur.ts b/src/hooks/use-element-blur.ts deleted file mode 100644 index 4cc99993b..000000000 --- a/src/hooks/use-element-blur.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { CSSProperties, EventHandler, MouseEventHandler, ReactEventHandler, useCallback, useState } from "react"; - -export default function useElementBlur(initBlur = false): { - style: CSSProperties; - onClick: MouseEventHandler; - handleEvent: ReactEventHandler; -} { - const [blur, setBlur] = useState(initBlur); - - const onClick = useCallback( - (e) => { - if (blur) { - e.stopPropagation(); - e.preventDefault(); - setBlur(false); - } - }, - [blur], - ); - const handleEvent = useCallback>( - (e) => { - if (blur) { - e.stopPropagation(); - e.preventDefault(); - setBlur(false); - } - }, - [blur], - ); - - const style: CSSProperties = blur ? { filter: "blur(1.5rem)", cursor: "pointer" } : {}; - - return { onClick, handleEvent, style }; -} diff --git a/src/hooks/use-element-trust-blur.ts b/src/hooks/use-element-trust-blur.ts new file mode 100644 index 000000000..eedb668ce --- /dev/null +++ b/src/hooks/use-element-trust-blur.ts @@ -0,0 +1,41 @@ +import { CSSProperties, MouseEventHandler, ReactEventHandler, useCallback } from "react"; +import { useTrustContext } from "../providers/local/trust"; + +export default function useElementTrustBlur(): { + style: CSSProperties; + onClick: MouseEventHandler; + handleEvent: ReactEventHandler; +} { + const { trust, setOverride } = useTrustContext(); + + const onClick = useCallback( + (e) => { + if (!trust) { + e.stopPropagation(); + e.preventDefault(); + if (!trust) setOverride(true); + + // prevent videos from auto playing + if (e.target instanceof HTMLVideoElement) e.target.pause(); + } + }, + [trust], + ); + const handleEvent = useCallback>( + (e) => { + if (!trust) { + e.stopPropagation(); + e.preventDefault(); + if (!trust) setOverride(true); + + // prevent videos from auto playing + if (e.target instanceof HTMLVideoElement) e.target.pause(); + } + }, + [trust], + ); + + const style: CSSProperties = !trust ? { filter: "blur(1.5rem)", cursor: "pointer" } : {}; + + return { onClick, handleEvent, style }; +} diff --git a/src/hooks/use-event-bookmark-actions.ts b/src/hooks/use-event-bookmark-actions.ts index 41d4c1146..c9d5c415d 100644 --- a/src/hooks/use-event-bookmark-actions.ts +++ b/src/hooks/use-event-bookmark-actions.ts @@ -4,7 +4,7 @@ import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../types/nostr-event"; import { useSigningContext } from "../providers/global/signing-provider"; import userUserBookmarksList from "./use-user-bookmarks-list"; -import { getEventCoordinate, isReplaceable, pointerMatchEvent } from "../helpers/nostr/events"; +import { getEventCoordinate, isReplaceable, pointerMatchEvent } from "../helpers/nostr/event"; import { BOOKMARK_LIST_KIND, listAddCoordinate, diff --git a/src/hooks/use-event-count.ts b/src/hooks/use-event-count.ts index 8916f9e3c..d7a7e0d01 100644 --- a/src/hooks/use-event-count.ts +++ b/src/hooks/use-event-count.ts @@ -1,6 +1,6 @@ import { useMemo } from "react"; import eventCountService from "../services/event-count"; -import { NostrRequestFilter } from "../types/nostr-query"; +import { NostrRequestFilter } from "../types/nostr-relay"; import useSubject from "./use-subject"; export default function useEventCount(filter?: NostrRequestFilter, alwaysRequest = false) { diff --git a/src/hooks/use-event-exists.ts b/src/hooks/use-event-exists.ts index 871d4a3e3..b1e5f0462 100644 --- a/src/hooks/use-event-exists.ts +++ b/src/hooks/use-event-exists.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import stringify from "json-stringify-deterministic"; import eventExistsService from "../services/event-exists"; -import { NostrRequestFilter } from "../types/nostr-query"; +import { NostrRequestFilter } from "../types/nostr-relay"; import useSubject from "./use-subject"; export default function useEventExists(filter?: NostrRequestFilter, relays: string[] = [], fallback = true) { diff --git a/src/hooks/use-events-reactions.ts b/src/hooks/use-events-reactions.ts index 18f4fb096..2637f6eb5 100644 --- a/src/hooks/use-events-reactions.ts +++ b/src/hooks/use-events-reactions.ts @@ -1,8 +1,9 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import eventReactionsService from "../services/event-reactions"; import { useReadRelays } from "./use-client-relays"; import { NostrEvent } from "../types/nostr-event"; import Subject from "../classes/subject"; +import useSubjects from "./use-subjects"; export default function useEventsReactions( eventIds: string[], @@ -29,17 +30,7 @@ export default function useEventsReactions( const [_, update] = useState(0); // subscribe to subjects - useEffect(() => { - const listener = () => update((v) => v + 1); - for (const [_, sub] of Object.entries(subjects)) { - sub?.subscribe(listener, undefined, false); - } - return () => { - for (const [_, sub] of Object.entries(subjects)) { - sub?.unsubscribe(listener, undefined); - } - }; - }, [subjects, update]); + useSubjects(Object.values(subjects)); return reactions; } diff --git a/src/hooks/use-favorite-emoji-packs.ts b/src/hooks/use-favorite-emoji-packs.ts index efdf8be7d..fde9a56ca 100644 --- a/src/hooks/use-favorite-emoji-packs.ts +++ b/src/hooks/use-favorite-emoji-packs.ts @@ -1,7 +1,7 @@ import useReplaceableEvent from "./use-replaceable-event"; import useCurrentAccount from "./use-current-account"; import { USER_EMOJI_LIST_KIND } from "../helpers/nostr/emoji-packs"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; export const FAVORITE_LISTS_IDENTIFIER = "nostrudel-favorite-lists"; diff --git a/src/hooks/use-params-address-pointer.ts b/src/hooks/use-params-address-pointer.ts index d2f3ee165..478d9f9eb 100644 --- a/src/hooks/use-params-address-pointer.ts +++ b/src/hooks/use-params-address-pointer.ts @@ -2,7 +2,7 @@ import { useParams } from "react-router-dom"; import { nip19 } from "nostr-tools"; import type { AddressPointer } from "nostr-tools/lib/types/nip19"; -import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events"; +import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/event"; export default function useParamsAddressPointer(key: string): AddressPointer; export default function useParamsAddressPointer(key: string, requireD: true): AddressPointer; diff --git a/src/hooks/use-replaceable-event.ts b/src/hooks/use-replaceable-event.ts index bdb8b8a47..0c99f3c44 100644 --- a/src/hooks/use-replaceable-event.ts +++ b/src/hooks/use-replaceable-event.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useReadRelays } from "./use-client-relays"; -import replaceableEventLoaderService, { RequestOptions } from "../services/replaceable-event-requester"; -import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events"; +import replaceableEventsService, { RequestOptions } from "../services/replaceable-events"; +import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/event"; import useSubject from "./use-subject"; export default function useReplaceableEvent( @@ -14,7 +14,7 @@ export default function useReplaceableEvent( const sub = useMemo(() => { const parsed = typeof cord === "string" ? parseCoordinate(cord) : cord; if (!parsed) return; - return replaceableEventLoaderService.requestEvent( + return replaceableEventsService.requestEvent( parsed.relays ? [...readRelays, ...parsed.relays] : readRelays, parsed.kind, parsed.pubkey, diff --git a/src/hooks/use-replaceable-events.ts b/src/hooks/use-replaceable-events.ts index fac48a454..c9f9f1ea2 100644 --- a/src/hooks/use-replaceable-events.ts +++ b/src/hooks/use-replaceable-events.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useReadRelays } from "./use-client-relays"; -import replaceableEventLoaderService, { RequestOptions } from "../services/replaceable-event-requester"; -import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events"; +import replaceableEventsService, { RequestOptions } from "../services/replaceable-events"; +import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/event"; import Subject from "../classes/subject"; import { NostrEvent } from "../types/nostr-event"; import useSubjects from "./use-subjects"; @@ -20,7 +20,7 @@ export default function useReplaceableEvents( const parsed = typeof cord === "string" ? parseCoordinate(cord) : cord; if (!parsed) return; subs.push( - replaceableEventLoaderService.requestEvent( + replaceableEventsService.requestEvent( parsed.relays ? [...readRelays, ...parsed.relays] : readRelays, parsed.kind, parsed.pubkey, diff --git a/src/hooks/use-subject.ts b/src/hooks/use-subject.ts index 06a062dd9..a5e8bf98e 100644 --- a/src/hooks/use-subject.ts +++ b/src/hooks/use-subject.ts @@ -1,16 +1,16 @@ -import { useEffect, useState } from "react"; -import { PersistentSubject, Subject } from "../classes/subject"; +import { useEffect, useRef, useState } from "react"; +import Subject, { PersistentSubject } from "../classes/subject"; function useSubject(subject: PersistentSubject): Value; function useSubject(subject?: PersistentSubject): Value | undefined; function useSubject(subject?: Subject): Value | undefined; function useSubject(subject?: Subject) { const [_, setValue] = useState(subject?.value); + const subRef = useRef(subject); useEffect(() => { - subject?.subscribe(setValue, undefined, false); - return () => { - subject?.unsubscribe(setValue, undefined); - }; + if (subject?.value !== undefined) setValue(subject?.value); + const sub = subject?.subscribe((v) => setValue(v)); + return () => sub?.unsubscribe(); }, [subject, setValue]); return subject?.value; diff --git a/src/hooks/use-subjects.ts b/src/hooks/use-subjects.ts index a005817c7..3e5a0a2c4 100644 --- a/src/hooks/use-subjects.ts +++ b/src/hooks/use-subjects.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { PersistentSubject, Subject } from "../classes/subject"; +import Subject, { PersistentSubject } from "../classes/subject"; function useSubjects( subjects: (Subject | PersistentSubject | undefined)[] = [], @@ -9,13 +9,9 @@ function useSubjects( useEffect(() => { const listener = () => update((v) => v + 1); - for (const sub of subjects) { - sub?.subscribe(listener, undefined, false); - } + const subs = subjects.map((s) => s?.subscribe(listener)); return () => { - for (const sub of subjects) { - sub?.unsubscribe(listener, undefined); - } + for (const sub of subs) sub?.unsubscribe(); }; }, [subjects, update]); diff --git a/src/hooks/use-thread-timeline-loader.ts b/src/hooks/use-thread-timeline-loader.ts index 3e0e91695..b08bab705 100644 --- a/src/hooks/use-thread-timeline-loader.ts +++ b/src/hooks/use-thread-timeline-loader.ts @@ -5,7 +5,7 @@ import useSubject from "./use-subject"; import useSingleEvent from "./use-single-event"; import singleEventService from "../services/single-event"; import useTimelineLoader from "./use-timeline-loader"; -import { getThreadReferences } from "../helpers/nostr/events"; +import { getThreadReferences } from "../helpers/nostr/event"; import { NostrEvent } from "../types/nostr-event"; import { unique } from "../helpers/array"; diff --git a/src/hooks/use-timeline-cursor-intersection-callback.ts b/src/hooks/use-timeline-cursor-intersection-callback.ts index d4014472f..a72d60c3b 100644 --- a/src/hooks/use-timeline-cursor-intersection-callback.ts +++ b/src/hooks/use-timeline-cursor-intersection-callback.ts @@ -7,7 +7,7 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader) // if the cursor is set too far ahead and the last block did not overlap with the cursor // we need to keep loading blocks until the timeline is complete or the blocks pass the cursor useInterval(() => { - timeline.triggerBlockLoads(); + timeline.triggerChunkLoad(); }, 1000); return useIntersectionMapCallback( @@ -25,7 +25,7 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader) if (oldestEvent) { timeline.setCursor(oldestEvent.created_at); - timeline.triggerBlockLoads(); + timeline.triggerChunkLoad(); } }, [timeline], diff --git a/src/hooks/use-timeline-loader.ts b/src/hooks/use-timeline-loader.ts index 19bc5d9ba..13d214841 100644 --- a/src/hooks/use-timeline-loader.ts +++ b/src/hooks/use-timeline-loader.ts @@ -1,10 +1,10 @@ import { useEffect, useMemo } from "react"; import { useUnmount } from "react-use"; +import { NostrEvent } from "nostr-tools"; -import { NostrRequestFilter } from "../types/nostr-query"; +import { NostrRequestFilter } from "../types/nostr-relay"; import timelineCacheService from "../services/timeline-cache"; import { EventFilter } from "../classes/timeline-loader"; -import { NostrEvent } from "../types/nostr-event"; import { createSimpleQueryMap } from "../helpers/nostr/filter"; type Options = { diff --git a/src/hooks/use-user-bookmarks-list.ts b/src/hooks/use-user-bookmarks-list.ts index 2ce429890..0893aef8d 100644 --- a/src/hooks/use-user-bookmarks-list.ts +++ b/src/hooks/use-user-bookmarks-list.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { BOOKMARK_LIST_KIND, getAddressPointersFromList, getEventPointersFromList } from "../helpers/nostr/lists"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; diff --git a/src/hooks/use-user-channels-list.ts b/src/hooks/use-user-channels-list.ts index 651cdcc15..0fc0ace92 100644 --- a/src/hooks/use-user-channels-list.ts +++ b/src/hooks/use-user-channels-list.ts @@ -1,5 +1,5 @@ import { CHANNELS_LIST_KIND, getEventPointersFromList } from "../helpers/nostr/lists"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; diff --git a/src/hooks/use-user-communities-list.ts b/src/hooks/use-user-communities-list.ts index 3abe13e45..565b3336d 100644 --- a/src/hooks/use-user-communities-list.ts +++ b/src/hooks/use-user-communities-list.ts @@ -1,6 +1,6 @@ import { COMMUNITY_DEFINITION_KIND, SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../helpers/nostr/communities"; import { COMMUNITIES_LIST_KIND, NOTE_LIST_KIND, getAddressPointersFromList } from "../helpers/nostr/lists"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; diff --git a/src/hooks/use-user-contact-list.ts b/src/hooks/use-user-contact-list.ts index 33d487cf5..1c9104985 100644 --- a/src/hooks/use-user-contact-list.ts +++ b/src/hooks/use-user-contact-list.ts @@ -1,6 +1,6 @@ import { kinds } from "nostr-tools"; import useReplaceableEvent from "./use-replaceable-event"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; export default function useUserContactList( pubkey?: string, diff --git a/src/hooks/use-user-contact-relays.ts b/src/hooks/use-user-contact-relays.ts index a3d731139..4d6a953b2 100644 --- a/src/hooks/use-user-contact-relays.ts +++ b/src/hooks/use-user-contact-relays.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import RelaySet from "../classes/relay-set"; import useUserContactList from "./use-user-contact-list"; import { RelayMode } from "../classes/relay"; diff --git a/src/hooks/use-user-dns-identity.ts b/src/hooks/use-user-dns-identity.ts index fcbf90a9a..6c6c0571a 100644 --- a/src/hooks/use-user-dns-identity.ts +++ b/src/hooks/use-user-dns-identity.ts @@ -1,5 +1,5 @@ -import { useDnsIdentity } from "./use-dns-identity"; -import { useUserMetadata } from "./use-user-metadata"; +import useDnsIdentity from "./use-dns-identity"; +import useUserMetadata from "./use-user-metadata"; export function useUserDNSIdentity(pubkey?: string) { const metadata = useUserMetadata(pubkey); diff --git a/src/hooks/use-user-lnurl-metadata.ts b/src/hooks/use-user-lnurl-metadata.ts index db6ce3f84..6da2acfb5 100644 --- a/src/hooks/use-user-lnurl-metadata.ts +++ b/src/hooks/use-user-lnurl-metadata.ts @@ -1,5 +1,5 @@ import { useAsync } from "react-use"; -import { useUserMetadata } from "./use-user-metadata"; +import useUserMetadata from "./use-user-metadata"; import lnurlMetadataService from "../services/lnurl-metadata"; export default function useUserLNURLMetadata(pubkey: string) { diff --git a/src/hooks/use-user-mailboxes.ts b/src/hooks/use-user-mailboxes.ts index 02bd13b22..a657bf603 100644 --- a/src/hooks/use-user-mailboxes.ts +++ b/src/hooks/use-user-mailboxes.ts @@ -1,5 +1,5 @@ import RelaySet from "../classes/relay-set"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import userMailboxesService from "../services/user-mailboxes"; import { useReadRelays } from "./use-client-relays"; import useSubject from "./use-subject"; diff --git a/src/hooks/use-user-metadata.ts b/src/hooks/use-user-metadata.ts index 9d7ac7a22..7346fb20d 100644 --- a/src/hooks/use-user-metadata.ts +++ b/src/hooks/use-user-metadata.ts @@ -2,10 +2,14 @@ import { useMemo } from "react"; import userMetadataService from "../services/user-metadata"; import { useReadRelays } from "./use-client-relays"; import useSubject from "./use-subject"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import { COMMON_CONTACT_RELAY } from "../const"; -export function useUserMetadata(pubkey?: string, additionalRelays: Iterable = [], opts: RequestOptions = {}) { +export default function useUserMetadata( + pubkey?: string, + additionalRelays: Iterable = [], + opts: RequestOptions = {}, +) { const relays = useReadRelays([...additionalRelays, COMMON_CONTACT_RELAY]); const subject = useMemo( diff --git a/src/hooks/use-user-mute-filter.ts b/src/hooks/use-user-mute-filter.ts index 96b07dc14..ec9287ba8 100644 --- a/src/hooks/use-user-mute-filter.ts +++ b/src/hooks/use-user-mute-filter.ts @@ -5,7 +5,7 @@ import useUserMuteList from "./use-user-mute-list"; import { getPubkeysFromList } from "../helpers/nostr/lists"; import { NostrEvent } from "../types/nostr-event"; import { STREAM_KIND, getStreamHost } from "../helpers/nostr/stream"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; export default function useUserMuteFilter(pubkey?: string, additionalRelays?: string[], opts?: RequestOptions) { const account = useCurrentAccount(); diff --git a/src/hooks/use-user-mute-list.ts b/src/hooks/use-user-mute-list.ts index 590bd81c5..aaa72f079 100644 --- a/src/hooks/use-user-mute-list.ts +++ b/src/hooks/use-user-mute-list.ts @@ -1,6 +1,6 @@ import useReplaceableEvent from "./use-replaceable-event"; import { MUTE_LIST_KIND } from "../helpers/nostr/lists"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; export default function useUserMuteList( pubkey?: string, diff --git a/src/hooks/use-user-mute-lists.ts b/src/hooks/use-user-mute-lists.ts index a135eab69..49b30b239 100644 --- a/src/hooks/use-user-mute-lists.ts +++ b/src/hooks/use-user-mute-lists.ts @@ -3,7 +3,7 @@ import { useMemo } from "react"; import useReplaceableEvent from "./use-replaceable-event"; import { PEOPLE_LIST_KIND, getPubkeysFromList } from "../helpers/nostr/lists"; import useUserMuteList from "./use-user-mute-list"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; export default function useUserMuteLists( pubkey?: string, diff --git a/src/hooks/use-user-network.ts b/src/hooks/use-user-network.ts index c7b59ca9d..509af2bf2 100644 --- a/src/hooks/use-user-network.ts +++ b/src/hooks/use-user-network.ts @@ -3,11 +3,11 @@ import { kinds } from "nostr-tools"; import { getPubkeysFromList } from "../helpers/nostr/lists"; import useUserContactList from "./use-user-contact-list"; -import replaceableEventLoaderService from "../services/replaceable-event-requester"; +import replaceableEventsService from "../services/replaceable-events"; import { useReadRelays } from "./use-client-relays"; import useSubjects from "./use-subjects"; import userMetadataService from "../services/user-metadata"; -import { Kind0ParsedContent } from "../helpers/user-metadata"; +import { Kind0ParsedContent } from "../helpers/nostr/user-metadata"; export function useUsersMetadata(pubkeys: string[], additionalRelays?: Iterable) { const readRelays = useReadRelays(additionalRelays); @@ -34,7 +34,7 @@ export default function useUserNetwork(pubkey: string, additionalRelays?: Iterab const subjects = useMemo(() => { return contactsPubkeys.map((person) => - replaceableEventLoaderService.requestEvent(readRelays, kinds.Contacts, person.pubkey), + replaceableEventsService.requestEvent(readRelays, kinds.Contacts, person.pubkey), ); }, [contactsPubkeys, readRelays.urls.join("|")]); diff --git a/src/hooks/use-user-pin-list.ts b/src/hooks/use-user-pin-list.ts index df08f0c14..683402ae4 100644 --- a/src/hooks/use-user-pin-list.ts +++ b/src/hooks/use-user-pin-list.ts @@ -1,5 +1,5 @@ import { PIN_LIST_KIND, getEventPointersFromList } from "../helpers/nostr/lists"; -import { RequestOptions } from "../services/replaceable-event-requester"; +import { RequestOptions } from "../services/replaceable-events"; import useCurrentAccount from "./use-current-account"; import useReplaceableEvent from "./use-replaceable-event"; diff --git a/src/hooks/use-user-profile-badges.ts b/src/hooks/use-user-profile-badges.ts index 70298e77a..ba2986580 100644 --- a/src/hooks/use-user-profile-badges.ts +++ b/src/hooks/use-user-profile-badges.ts @@ -4,7 +4,7 @@ import useReplaceableEvent from "./use-replaceable-event"; import { PROFILE_BADGES_IDENTIFIER, parseProfileBadges } from "../helpers/nostr/badges"; import useReplaceableEvents from "./use-replaceable-events"; import useSingleEvents from "./use-single-events"; -import { getEventCoordinate } from "../helpers/nostr/events"; +import { getEventCoordinate } from "../helpers/nostr/event"; import { NostrEvent } from "../types/nostr-event"; export default function useUserProfileBadges(pubkey: string, additionalRelays?: Iterable) { diff --git a/src/index.tsx b/src/index.tsx index a6f0ceded..2296dbbed 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { App } from "./app"; import { GlobalProviders } from "./providers/global"; import "./services/user-event-sync"; +import "./services/username-search"; // setup bitcoin connect import { init, onConnected } from "@getalby/bitcoin-connect-react"; diff --git a/src/providers/global/emoji-provider.tsx b/src/providers/global/emoji-provider.tsx index d5dfcdd2d..72c7bc87b 100644 --- a/src/providers/global/emoji-provider.tsx +++ b/src/providers/global/emoji-provider.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, createContext, useContext } from "react"; -import { lib } from "emojilib"; +import lib from "emojilib"; import useReplaceableEvents from "../../hooks/use-replaceable-events"; import useCurrentAccount from "../../hooks/use-current-account"; @@ -7,10 +7,10 @@ import { isEmojiTag } from "../../types/nostr-event"; import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs"; import { getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs"; -const defaultEmojis = Object.entries(lib).map(([name, emojiObject]) => ({ - ...emojiObject, - keywords: [name, ...emojiObject.keywords], +const defaultEmojis = Object.entries(lib).map(([char, [name, ...keywords]]) => ({ name, + keywords: [name, ...keywords], + char, })); export type Emoji = { name: string; keywords: string[]; char: string; url?: string }; diff --git a/src/providers/global/publish-provider.tsx b/src/providers/global/publish-provider.tsx index 4117e0986..4add3b3d8 100644 --- a/src/providers/global/publish-provider.tsx +++ b/src/providers/global/publish-provider.tsx @@ -7,8 +7,8 @@ import { DraftNostrEvent } from "../../types/nostr-event"; import NostrPublishAction from "../../classes/nostr-publish-action"; import clientRelaysService from "../../services/client-relays"; import RelaySet from "../../classes/relay-set"; -import { addPubkeyRelayHints, getAllRelayHints, isReplaceable } from "../../helpers/nostr/events"; -import replaceableEventLoaderService from "../../services/replaceable-event-requester"; +import { addPubkeyRelayHints, getAllRelayHints, isReplaceable } from "../../helpers/nostr/event"; +import replaceableEventsService from "../../services/replaceable-events"; import eventExistsService from "../../services/event-exists"; import eventReactionsService from "../../services/event-reactions"; import { localRelay } from "../../services/local-relay"; @@ -66,8 +66,8 @@ export default function PublishProvider({ children }: PropsWithChildren) { const pub = new NostrPublishAction(label, relays, signed); setLog((arr) => arr.concat(pub)); - pub.onResult.subscribe((result) => { - if (result.status) handleEventFromRelay(result.relay, signed); + pub.onResult.subscribe(({ relay, result }) => { + if (result[2]) handleEventFromRelay(relay, signed); }); // send it to the local relay @@ -75,7 +75,7 @@ export default function PublishProvider({ children }: PropsWithChildren) { // pass it to other services eventExistsService.handleEvent(signed); - if (isReplaceable(signed.kind)) replaceableEventLoaderService.handleEvent(signed); + if (isReplaceable(signed.kind)) replaceableEventsService.handleEvent(signed); if (signed.kind === kinds.Reaction) eventReactionsService.handleEvent(signed); if (signed.kind === kinds.EventDeletion) deleteEventService.handleEvent(signed); return pub; diff --git a/src/providers/global/user-directory-provider.tsx b/src/providers/global/user-directory-provider.tsx index 37f1ea495..8b2bb2df5 100644 --- a/src/providers/global/user-directory-provider.tsx +++ b/src/providers/global/user-directory-provider.tsx @@ -3,7 +3,7 @@ import { useAsync } from "react-use"; import db from "../../services/db"; -export type UserDirectory = { pubkey: string; names: [] }[]; +export type UserDirectory = { pubkey: string; names: string[] }[]; export type GetDirectoryFn = () => UserDirectory; const UserSearchDirectoryContext = createContext(() => []); diff --git a/src/providers/local/intersection-observer.tsx b/src/providers/local/intersection-observer.tsx index 3f76d5ed0..da6bda8e2 100644 --- a/src/providers/local/intersection-observer.tsx +++ b/src/providers/local/intersection-observer.tsx @@ -75,7 +75,7 @@ export default function IntersectionObserverProvider({ callback: ExtendedIntersectionObserverCallback; }) { const elementIds = useMemo(() => new WeakMap(), []); - const [subject] = useState(() => new Subject([], false)); + const [subject] = useState(() => new Subject([])); const handleIntersection = useCallback( (entries, observer) => { diff --git a/src/providers/local/people-list-provider.tsx b/src/providers/local/people-list-provider.tsx index a5e4e3356..0901a76af 100644 --- a/src/providers/local/people-list-provider.tsx +++ b/src/providers/local/people-list-provider.tsx @@ -5,7 +5,7 @@ import useCurrentAccount from "../../hooks/use-current-account"; import { getPubkeysFromList } from "../../helpers/nostr/lists"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; import { NostrEvent } from "../../types/nostr-event"; -import { NostrQuery } from "../../types/nostr-query"; +import { NostrQuery } from "../../types/nostr-relay"; import useRouteSearchValue from "../../hooks/use-route-search-value"; export type ListId = "following" | "global" | string; diff --git a/src/providers/local/trust.tsx b/src/providers/local/trust.tsx index 40dfee0ef..01da1a978 100644 --- a/src/providers/local/trust.tsx +++ b/src/providers/local/trust.tsx @@ -1,12 +1,15 @@ -import React, { PropsWithChildren, useContext } from "react"; +import React, { PropsWithChildren, useContext, useMemo, useState } from "react"; import { NostrEvent } from "../../types/nostr-event"; import useCurrentAccount from "../../hooks/use-current-account"; import useUserContactList from "../../hooks/use-user-contact-list"; import { getPubkeysFromList } from "../../helpers/nostr/lists"; -const TrustContext = React.createContext(false); +const TrustContext = React.createContext<{ trust: boolean; setOverride: (trust: boolean) => void }>({ + trust: false, + setOverride: () => {}, +}); -export function useTrusted() { +export function useTrustContext() { return useContext(TrustContext); } @@ -14,8 +17,10 @@ export function TrustProvider({ children, event, trust = false, -}: PropsWithChildren & { event?: NostrEvent; trust?: boolean }) { - const parentTrust = useContext(TrustContext); + allowOverride = true, +}: PropsWithChildren & { event?: NostrEvent; trust?: boolean; allowOverride?: boolean }) { + const { trust: parentTrust } = useContext(TrustContext); + const [override, setOverride] = useState(); const account = useCurrentAccount(); const contactList = useUserContactList(account?.pubkey); @@ -23,5 +28,13 @@ export function TrustProvider({ const isEventTrusted = trust || (!!event && (event.pubkey === account?.pubkey || following.includes(event.pubkey))); - return {children}; + const context = useMemo(() => { + const trust = parentTrust || isEventTrusted; + return { + trust: allowOverride ? override ?? trust : trust, + setOverride: (v: boolean) => allowOverride && setOverride(v), + }; + }, [override, parentTrust, isEventTrusted, setOverride, allowOverride]); + + return {children}; } diff --git a/src/providers/route/app-handler-provider.tsx b/src/providers/route/app-handler-provider.tsx new file mode 100644 index 000000000..886b97479 --- /dev/null +++ b/src/providers/route/app-handler-provider.tsx @@ -0,0 +1,40 @@ +import { useToast } from "@chakra-ui/react"; +import { nip19 } from "nostr-tools"; +import { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react"; +import AppHandlerModal from "../../components/app-handler-modal"; + +type AppHandlerContextType = { + openAddress(address: string): void; +}; +export const AppHandlerContext = createContext({ + openAddress() { + throw new Error("AppHandler provider missing"); + }, +}); + +export default function AppHandlerProvider({ children }: PropsWithChildren) { + const toast = useToast(); + const [decoded, setDecoded] = useState(); + + const openAddress = useCallback((address: string) => { + try { + setDecoded(nip19.decode(address)); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + }, []); + + const context = useMemo( + () => ({ + openAddress, + }), + [openAddress], + ); + + return ( + + {children} + {decoded && setDecoded(undefined)} />} + + ); +} diff --git a/src/providers/route/delete-event-provider.tsx b/src/providers/route/delete-event-provider.tsx index 5f92eb595..f5bbcab8e 100644 --- a/src/providers/route/delete-event-provider.tsx +++ b/src/providers/route/delete-event-provider.tsx @@ -26,7 +26,7 @@ import createDefer, { Deferred } from "../../classes/deferred"; import useEventRelays from "../../hooks/use-event-relays"; import { RelayFavicon } from "../../components/relay-favicon"; import { ExternalLinkIcon } from "../../components/icons"; -import { getEventCoordinate, getEventUID, isReplaceable } from "../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID, isReplaceable } from "../../helpers/nostr/event"; import { Tag } from "../../types/nostr-event"; import { EmbedEvent } from "../../components/embed-event"; import { useWriteRelays } from "../../hooks/use-client-relays"; diff --git a/src/providers/route/index.tsx b/src/providers/route/index.tsx index 3a04b5b1b..7448adf7a 100644 --- a/src/providers/route/index.tsx +++ b/src/providers/route/index.tsx @@ -1,3 +1,4 @@ +import AppHandlerProvider from "./app-handler-provider"; import DebugModalProvider from "./debug-modal-provider"; import DeleteEventProvider from "./delete-event-provider"; import InvoiceModalProvider from "./invoice-modal"; @@ -13,7 +14,9 @@ export function RouteProviders({ children }: { children: React.ReactNode }) { - {children} + + {children} + diff --git a/src/providers/route/mute-modal-provider.tsx b/src/providers/route/mute-modal-provider.tsx index 236f72344..dc912fb5c 100644 --- a/src/providers/route/mute-modal-provider.tsx +++ b/src/providers/route/mute-modal-provider.tsx @@ -20,8 +20,8 @@ import { PropsWithChildren, createContext, useCallback, useContext, useMemo, use import dayjs from "dayjs"; import { useInterval } from "react-use"; -import { getUserDisplayName } from "../../helpers/user-metadata"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../helpers/nostr/user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; import useCurrentAccount from "../../hooks/use-current-account"; import { createEmptyMuteList, @@ -34,8 +34,8 @@ import { cloneList } from "../../helpers/nostr/lists"; import { useSigningContext } from "../global/signing-provider"; import useUserMuteList from "../../hooks/use-user-mute-list"; import { DraftNostrEvent } from "../../types/nostr-event"; -import UserAvatar from "../../components/user-avatar"; -import UserLink from "../../components/user-link"; +import UserAvatar from "../../components/user/user-avatar"; +import UserLink from "../../components/user/user-link"; import { ChevronDownIcon } from "../../components/icons"; import { usePublishEvent } from "../global/publish-provider"; diff --git a/src/services/channel-metadata.ts b/src/services/channel-metadata.ts index 5a12d8505..4fd1f7d52 100644 --- a/src/services/channel-metadata.ts +++ b/src/services/channel-metadata.ts @@ -7,7 +7,7 @@ import NostrSubscription from "../classes/nostr-subscription"; import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; import Subject from "../classes/subject"; -import { NostrQuery } from "../types/nostr-query"; +import { NostrQuery } from "../types/nostr-relay"; import { logger } from "../helpers/debug"; import db from "./db"; import createDefer, { Deferred } from "../classes/deferred"; @@ -30,7 +30,7 @@ const RELAY_REQUEST_BATCH_TIME = 1000; /** This class is ued to batch requests to a single relay */ class ChannelMetadataRelayLoader { private subscription: NostrSubscription; - private events = new SuperMap>(() => new Subject()); + private events = new SuperMap>(() => new Subject()); private requestNext = new Set(); private requested = new Map(); @@ -109,7 +109,7 @@ class ChannelMetadataRelayLoader { }; if (query["#e"] && query["#e"].length > 0) this.log(`Updating query`, query["#e"].length); - this.subscription.setQuery(query); + this.subscription.setFilters([query]); if (this.subscription.state !== NostrSubscription.OPEN) { this.subscription.open(); @@ -234,7 +234,7 @@ class ChannelMetadataService { for (const relay of relayUrls) { const request = this.loaders.get(relay).requestMetadata(channelId); - sub.connectWithHandler(request, (event, next, current) => { + sub.connectWithMapper(request, (event, next, current) => { if (!current || event.created_at > current.created_at) { next(event); this.saveToCache(channelId, event); diff --git a/src/services/db/index.ts b/src/services/db/index.ts index b0f9f710b..570308597 100644 --- a/src/services/db/index.ts +++ b/src/services/db/index.ts @@ -203,6 +203,7 @@ export async function deleteDatabase() { db.close(); log("Deleting"); await deleteDB(dbName); + localDatabase.close(); await nostrIDBDelete(); window.location.reload(); } diff --git a/src/services/delete-events.ts b/src/services/delete-events.ts index c914a9119..350b459c6 100644 --- a/src/services/delete-events.ts +++ b/src/services/delete-events.ts @@ -1,10 +1,10 @@ import { kinds } from "nostr-tools"; -import Subject from "../classes/subject"; -import { getEventUID } from "../helpers/nostr/events"; +import { getEventUID } from "../helpers/nostr/event"; import { NostrEvent } from "../types/nostr-event"; +import ControlledObservable from "../classes/controlled-observable"; -const deleteEventStream = new Subject(); +const deleteEventStream = new ControlledObservable(); function handleEvent(deleteEvent: NostrEvent) { if (deleteEvent.kind !== kinds.EventDeletion) return; diff --git a/src/services/event-count.ts b/src/services/event-count.ts index 87c8f5cda..39f73161c 100644 --- a/src/services/event-count.ts +++ b/src/services/event-count.ts @@ -2,7 +2,7 @@ import stringify from "json-stringify-deterministic"; import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; -import { NostrRequestFilter } from "../types/nostr-query"; +import { NostrRequestFilter } from "../types/nostr-relay"; import NostrRequest from "../classes/nostr-request"; import relayPoolService from "./relay-pool"; diff --git a/src/services/event-exists.ts b/src/services/event-exists.ts index 3d5393ff9..9032520c6 100644 --- a/src/services/event-exists.ts +++ b/src/services/event-exists.ts @@ -1,7 +1,7 @@ import stringify from "json-stringify-deterministic"; import Subject from "../classes/subject"; -import { NostrRequestFilter } from "../types/nostr-query"; +import { NostrRequestFilter } from "../types/nostr-relay"; import SuperMap from "../classes/super-map"; import NostrRequest from "../classes/nostr-request"; import relayScoreboardService from "./relay-scoreboard"; diff --git a/src/services/event-reactions.ts b/src/services/event-reactions.ts index d4a6841e6..60ebae9f1 100644 --- a/src/services/event-reactions.ts +++ b/src/services/event-reactions.ts @@ -74,7 +74,7 @@ class EventReactionsService { if (filters.length > 0) { const request = new NostrRequest([relay]); - request.onEvent.subscribe(this.handleEvent, this); + request.onEvent.subscribe((e) => this.handleEvent(e)); request.start(filters); } } diff --git a/src/services/event-relay-hint.ts b/src/services/event-relay-hint.ts index dde8407b2..3419583cb 100644 --- a/src/services/event-relay-hint.ts +++ b/src/services/event-relay-hint.ts @@ -1,8 +1,8 @@ +import { createCoordinate } from "../classes/batch-kind-loader"; import { NostrEvent } from "../types/nostr-event"; import { getEventRelays } from "./event-relays"; import relayScoreboardService from "./relay-scoreboard"; import type { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19"; -import { createCoordinate } from "./replaceable-event-requester"; function pickBestRelays(relays: string[]) { // ignore local relays diff --git a/src/services/event-relays.ts b/src/services/event-relays.ts index 8fbb56910..ec2bd5555 100644 --- a/src/services/event-relays.ts +++ b/src/services/event-relays.ts @@ -1,6 +1,6 @@ import Relay from "../classes/relay"; import { PersistentSubject } from "../classes/subject"; -import { getEventUID } from "../helpers/nostr/events"; +import { getEventUID } from "../helpers/nostr/event"; import { NostrEvent } from "../types/nostr-event"; import relayPoolService from "./relay-pool"; @@ -31,8 +31,8 @@ export function handleEventFromRelay(relay: Relay, event: NostrEvent) { } relayPoolService.onRelayCreated.subscribe((relay) => { - relay.onEvent.subscribe(({ body: event }) => { - handleEventFromRelay(relay, event); + relay.onEvent.subscribe((message) => { + handleEventFromRelay(relay, message[2]); }); }); diff --git a/src/services/event-zaps.ts b/src/services/event-zaps.ts index 463a905b6..9a9679921 100644 --- a/src/services/event-zaps.ts +++ b/src/services/event-zaps.ts @@ -74,7 +74,7 @@ class EventZapsService { if (filter.length > 0) { const request = new NostrRequest([relay]); - request.onEvent.subscribe(this.handleEvent, this); + request.onEvent.subscribe((e) => this.handleEvent(e)); request.start(filter); } } diff --git a/src/services/local-relay.ts b/src/services/local-relay.ts index 2b1618cfd..5e2484434 100644 --- a/src/services/local-relay.ts +++ b/src/services/local-relay.ts @@ -1,7 +1,6 @@ -import { CacheRelay, openDB, pruneLastUsed } from "nostr-idb"; +import { CacheRelay, openDB } from "nostr-idb"; import { Relay } from "nostr-tools"; import { logger } from "../helpers/debug"; -import _throttle from "lodash.throttle"; import { safeRelayUrl } from "../helpers/relay"; // save the local relay from query params to localStorage diff --git a/src/services/nostr-connect.ts b/src/services/nostr-connect.ts index 8699f2c39..dd59ca982 100644 --- a/src/services/nostr-connect.ts +++ b/src/services/nostr-connect.ts @@ -1,6 +1,7 @@ import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04, nip19 } from "nostr-tools"; import dayjs from "dayjs"; import { nanoid } from "nanoid"; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import NostrMultiSubscription from "../classes/nostr-multi-subscription"; import { getPubkeyFromDecodeResult, isHexKey, normalizeToHexPubkey } from "../helpers/nip19"; @@ -8,11 +9,9 @@ import { createSimpleQueryMap } from "../helpers/nostr/filter"; import { logger } from "../helpers/debug"; import { DraftNostrEvent, NostrEvent, isPTag } from "../types/nostr-event"; import createDefer, { Deferred } from "../classes/deferred"; -import { truncatedId } from "../helpers/nostr/events"; +import { truncatedId } from "../helpers/nostr/event"; import { NostrConnectAccount } from "./account"; -import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { safeRelayUrl } from "../helpers/relay"; -import Subject from "../classes/subject"; export function isErrorResponse(response: any): response is NostrConnectErrorResponse { return !!response.error; @@ -80,7 +79,7 @@ export class NostrConnectClient { this.secretKey = secretKey || bytesToHex(generateSecretKey()); this.publicKey = getPublicKey(hexToBytes(this.secretKey)); - this.sub.onEvent.subscribe(this.handleEvent, this); + this.sub.onEvent.subscribe((e) => this.handleEvent(e)); this.sub.setQueryMap( createSimpleQueryMap(this.relays, { kinds: [kinds.NostrConnect, 24134], @@ -262,6 +261,7 @@ class NostrConnectService { fromHostedBunker(pubkey: string, relays: string[], provider?: string) { return this.getClient(pubkey) || this.createClient(pubkey, relays, undefined, provider); } + /** create client from: pubkey@wss://relay.com (with optional bunker://) */ fromBunkerAddress(address: string) { const parts = address.replace("bunker://", "").split("@"); if (parts.length !== 2) throw new Error("Invalid bunker address"); @@ -272,21 +272,21 @@ class NostrConnectService { return this.getClient(pubkey) || this.createClient(pubkey, [pathRelay]); } + /** create client from: bunker://?relay= */ fromBunkerURI(uri: string) { const url = new URL(uri); - const pathParts = url.pathname.replace(/^\/\//, "").split("@"); - const pubkey = pathParts[0]; - const pathRelay = pathParts[1] as string | undefined; + // firefox puts pubkey part in host, chrome puts pubkey in pathname + const pubkey = url.host || url.pathname.replace("//", ""); if (!isHexKey(pubkey)) throw new Error("Invalid connection URI"); const relays = url.searchParams.getAll("relay"); - if (pathRelay) relays.push(pathRelay); if (relays.length === 0) throw new Error("Missing relays"); return this.getClient(pubkey) || this.createClient(pubkey, relays); } - fromBunkerToken(token: string) { - const [npub, hexToken] = token.split("#"); + /** create client from: pubkey#token */ + fromBunkerToken(pubkeyWithToken: string) { + const [npub, hexToken] = pubkeyWithToken.split("#"); const decoded = nip19.decode(npub); const pubkey = getPubkeyFromDecodeResult(decoded); if (!pubkey) throw new Error("Cant find pubkey"); @@ -296,6 +296,7 @@ class NostrConnectService { const client = this.getClient(pubkey) || this.createClient(pubkey, relays); return client; } + /** create client from NIP-05 */ fromAccount(account: NostrConnectAccount) { const existingClient = this.getClient(account.pubkey); if (existingClient) return existingClient; diff --git a/src/services/relay-pool.ts b/src/services/relay-pool.ts index 78b4005a4..07ad1beeb 100644 --- a/src/services/relay-pool.ts +++ b/src/services/relay-pool.ts @@ -1,95 +1,7 @@ -import Relay from "../classes/relay"; -import Subject from "../classes/subject"; -import { logger } from "../helpers/debug"; -import { safeRelayUrl, validateRelayURL } from "../helpers/relay"; +import RelayPool from "../classes/relay-pool"; import { offlineMode } from "./offline-mode"; -export class RelayPoolService { - relays = new Map(); - relayClaims = new Map>(); - onRelayCreated = new Subject(); - - 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; - } - - 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); - } - } - } - } - - // id can be anything - 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; - } -} - -const relayPoolService = new RelayPoolService(); +const relayPoolService = new RelayPool(); setInterval(() => { if (document.visibilityState === "visible") { diff --git a/src/services/relay-stats.ts b/src/services/relay-stats.ts index 06bce9bf0..ee39b1cd1 100644 --- a/src/services/relay-stats.ts +++ b/src/services/relay-stats.ts @@ -51,7 +51,7 @@ class RelayStatsService { if (!info.pubkey) return sub.next(null); const request = new NostrRequest([relay, MONITOR_RELAY]); - request.onEvent.subscribe(this.handleEvent, this); + request.onEvent.subscribe((e) => this.handleEvent(e)); request.start({ kinds: [SELF_REPORTED_KIND], authors: [info.pubkey] }); }); } @@ -75,7 +75,7 @@ class RelayStatsService { const relays = Array.from(this.pendingMonitorStats); const request = new NostrRequest([MONITOR_RELAY]); - request.onEvent.subscribe(this.handleEvent, this); + request.onEvent.subscribe((e) => this.handleEvent(e)); request.start({ since: 1704196800, kinds: [MONITOR_STATS_KIND], "#d": relays, authors: [MONITOR_PUBKEY] }); this.pendingMonitorStats.clear(); diff --git a/src/services/replaceable-event-requester.ts b/src/services/replaceable-event-requester.ts deleted file mode 100644 index 3b3eb1f18..000000000 --- a/src/services/replaceable-event-requester.ts +++ /dev/null @@ -1,292 +0,0 @@ -import dayjs from "dayjs"; -import debug, { Debugger } from "debug"; -import _throttle from "lodash/throttle"; -import { Filter } from "nostr-tools"; - -import NostrSubscription from "../classes/nostr-subscription"; -import SuperMap from "../classes/super-map"; -import { NostrEvent } from "../types/nostr-event"; -import Subject from "../classes/subject"; -import { NostrQuery } from "../types/nostr-query"; -import { logger } from "../helpers/debug"; -import { nameOrPubkey } from "./user-metadata"; -import { getEventCoordinate } from "../helpers/nostr/events"; -import createDefer, { Deferred } from "../classes/deferred"; -import { localRelay } from "./local-relay"; -import { relayRequest } from "../helpers/relay"; - -type Pubkey = string; -type Relay = string; - -export type RequestOptions = { - /** Always request the event from the relays */ - alwaysRequest?: boolean; - /** ignore the cache on initial load */ - ignoreCache?: boolean; - // TODO: figure out a clean way for useReplaceableEvent hook to "unset" or "unsubscribe" - // keepAlive?: boolean; -}; - -export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: string) { - return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`; -} -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 to a single relay */ -class ReplaceableEventRelayLoader { - private subscription: NostrSubscription; - private events = new SuperMap>(() => new Subject()); - - private requestNext = new Set(); - private requested = new Map(); - - 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("misc"); - } - - private handleEvent(event: NostrEvent) { - const cord = getEventCoordinate(event); - - // remove the pubkey from the waiting list - this.requested.delete(cord); - - const sub = this.events.get(cord); - - const current = sub.value; - if (!current || event.created_at > current.created_at) { - sub.next(event); - } - } - private handleEOSE() { - // relays says it has nothing left - this.requested.clear(); - } - - getEvent(kind: number, pubkey: string, d?: string) { - return this.events.get(createCoordinate(kind, pubkey, d)); - } - - requestEvent(kind: number, pubkey: string, d?: string) { - const cord = createCoordinate(kind, pubkey, d); - const event = this.events.get(cord); - - if (!event.value) { - this.requestNext.add(cord); - this.updateThrottle(); - } - - return event; - } - - updateThrottle = _throttle(this.update, RELAY_REQUEST_BATCH_TIME); - update() { - let needsUpdate = false; - for (const cord of this.requestNext) { - if (!this.requested.has(cord)) { - this.requested.set(cord, new Date()); - needsUpdate = true; - } - } - this.requestNext.clear(); - - // prune requests - const timeout = dayjs().subtract(1, "minute"); - for (const [cord, date] of this.requested) { - if (dayjs(date).isBefore(timeout)) { - this.requested.delete(cord); - needsUpdate = true; - } - } - - // update the subscription - if (needsUpdate) { - if (this.requested.size > 0) { - const filters: Record = {}; - - 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.setQuery(query); - - if (this.subscription.state !== NostrSubscription.OPEN) { - this.subscription.open(); - } - } else if (this.subscription.state === NostrSubscription.OPEN) { - this.subscription.close(); - } - } - } -} - -const READ_CACHE_BATCH_TIME = 250; -const WRITE_CACHE_BATCH_TIME = 250; - -class ReplaceableEventLoaderService { - private events = new SuperMap>(() => new Subject()); - - private loaders = new SuperMap( - (relay) => new ReplaceableEventRelayLoader(relay, this.log.extend(relay)), - ); - - log = logger.extend("ReplaceableEventLoader"); - dbLog = this.log.extend("database"); - - handleEvent(event: NostrEvent, saveToCache = true) { - const cord = getEventCoordinate(event); - - const sub = this.events.get(cord); - const current = sub.value; - if (!current || event.created_at > current.created_at) { - sub.next(event); - if (saveToCache) { - this.saveToCache(cord, event); - } - } - } - - getEvent(kind: number, pubkey: string, d?: string) { - return this.events.get(createCoordinate(kind, pubkey, d)); - } - - private readFromCachePromises = new Map>(); - private readFromCacheThrottle = _throttle(this.readFromCache, READ_CACHE_BATCH_TIME); - private async readFromCache() { - if (this.readFromCachePromises.size === 0) return; - - const loading = new Map>(); - - const kindFilters: Record = {}; - for (const [cord, p] of this.readFromCachePromises) { - const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string]; - const kind = parseInt(kindStr); - kindFilters[kind] = kindFilters[kind] || { kinds: [kind] }; - - const arr = (kindFilters[kind].authors = kindFilters[kind].authors || []); - arr.push(pubkey); - - if (d) { - const arr = (kindFilters[kind]["#d"] = kindFilters[kind]["#d"] || []); - arr.push(d); - } - - loading.set(cord, p); - } - const filters = Object.values(kindFilters); - - for (const [cord] of loading) this.readFromCachePromises.delete(cord); - - const events = await relayRequest(localRelay, filters); - for (const event of events) { - this.handleEvent(event, false); - const cord = getEventCoordinate(event); - const promise = loading.get(cord); - if (promise) promise.resolve(true); - loading.delete(cord); - } - - // resolve remaining promises - for (const [_, promise] of loading) promise.resolve(); - - if (events.length > 0) this.dbLog(`Read ${events.length} events from database`); - } - loadFromCache(cord: string) { - const dedupe = this.readFromCachePromises.get(cord); - if (dedupe) return dedupe; - - // add to read queue - const promise = createDefer(); - this.readFromCachePromises.set(cord, promise); - - this.readFromCacheThrottle(); - - return promise; - } - - private writeCacheQueue = new Map(); - private writeToCacheThrottle = _throttle(this.writeToCache, WRITE_CACHE_BATCH_TIME); - private async writeToCache() { - if (this.writeCacheQueue.size === 0) return; - - this.dbLog(`Writing ${this.writeCacheQueue.size} events to database`); - for (const [_, event] of this.writeCacheQueue) localRelay.publish(event); - this.writeCacheQueue.clear(); - } - private async saveToCache(cord: string, event: NostrEvent) { - this.writeCacheQueue.set(cord, event); - this.writeToCacheThrottle(); - } - - private requestEventFromRelays(relays: Iterable, kind: number, pubkey: string, d?: string) { - const cord = createCoordinate(kind, pubkey, d); - const sub = this.events.get(cord); - - for (const relay of relays) { - const request = this.loaders.get(relay).requestEvent(kind, pubkey, d); - - sub.connectWithHandler(request, (event, next, current) => { - if (!current || event.created_at > current.created_at) { - next(event); - this.saveToCache(cord, event); - } - }); - } - - return sub; - } - - requestEvent(relays: Iterable, kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) { - const cord = createCoordinate(kind, pubkey, d); - const sub = this.events.get(cord); - - if (!sub.value) { - this.loadFromCache(cord).then((loaded) => { - if (!loaded && !sub.value) this.requestEventFromRelays(relays, kind, pubkey, d); - }); - } - - if (opts?.alwaysRequest || (!sub.value && opts.ignoreCache)) { - this.requestEventFromRelays(relays, kind, pubkey, d); - } - - return sub; - } -} - -const replaceableEventLoaderService = new ReplaceableEventLoaderService(); - -if (import.meta.env.DEV) { - //@ts-ignore - window.replaceableEventLoaderService = replaceableEventLoaderService; -} - -export default replaceableEventLoaderService; diff --git a/src/services/replaceable-events.ts b/src/services/replaceable-events.ts new file mode 100644 index 000000000..7648e42b1 --- /dev/null +++ b/src/services/replaceable-events.ts @@ -0,0 +1,162 @@ +import { Filter, NostrEvent } from "nostr-tools"; +import _throttle from "lodash.throttle"; + +import SuperMap from "../classes/super-map"; +import { logger } from "../helpers/debug"; +import { nameOrPubkey } from "./user-metadata"; +import { getEventCoordinate } from "../helpers/nostr/event"; +import createDefer, { Deferred } from "../classes/deferred"; +import { localRelay } from "./local-relay"; +import { relayRequest } from "../helpers/relay"; +import EventStore from "../classes/event-store"; +import Subject from "../classes/subject"; +import BatchKindLoader, { createCoordinate } from "../classes/batch-kind-loader"; + +export type RequestOptions = { + /** Always request the event from the relays */ + alwaysRequest?: boolean; + /** ignore the cache on initial load */ + ignoreCache?: boolean; + // TODO: figure out a clean way for useReplaceableEvent hook to "unset" or "unsubscribe" + // keepAlive?: boolean; +}; + +export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: string) { + return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`; +} + +const READ_CACHE_BATCH_TIME = 250; +const WRITE_CACHE_BATCH_TIME = 250; + +class ReplaceableEventsService { + private subjects = new SuperMap>(() => new Subject()); + private loaders = new SuperMap((relay) => { + const loader = new BatchKindLoader(relay, this.log.extend(relay)); + loader.events.onEvent.subscribe((e) => this.handleEvent(e)); + return loader; + }); + + events = new EventStore(); + + log = logger.extend("ReplaceableEventLoader"); + dbLog = this.log.extend("database"); + + handleEvent(event: NostrEvent, saveToCache = true) { + const cord = getEventCoordinate(event); + + const subject = this.subjects.get(cord); + const current = subject.value; + if (!current || event.created_at > current.created_at) { + subject.next(event); + this.events.addEvent(event); + if (saveToCache) this.saveToCache(cord, event); + } + } + + getEvent(kind: number, pubkey: string, d?: string) { + return this.subjects.get(createCoordinate(kind, pubkey, d)); + } + + private readFromCachePromises = new Map>(); + private readFromCacheThrottle = _throttle(this.readFromCache, READ_CACHE_BATCH_TIME); + private async readFromCache() { + if (this.readFromCachePromises.size === 0) return; + + const loading = new Map>(); + + const kindFilters: Record = {}; + for (const [cord, p] of this.readFromCachePromises) { + const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string]; + const kind = parseInt(kindStr); + kindFilters[kind] = kindFilters[kind] || { kinds: [kind] }; + + const arr = (kindFilters[kind].authors = kindFilters[kind].authors || []); + arr.push(pubkey); + + if (d) { + const arr = (kindFilters[kind]["#d"] = kindFilters[kind]["#d"] || []); + arr.push(d); + } + + loading.set(cord, p); + } + const filters = Object.values(kindFilters); + + for (const [cord] of loading) this.readFromCachePromises.delete(cord); + + const events = await relayRequest(localRelay, filters); + for (const event of events) { + this.handleEvent(event, false); + const cord = getEventCoordinate(event); + const promise = loading.get(cord); + if (promise) promise.resolve(true); + loading.delete(cord); + } + + // resolve remaining promises + for (const [_, promise] of loading) promise.resolve(); + + if (events.length > 0) this.dbLog(`Read ${events.length} events from database`); + } + loadFromCache(cord: string) { + const dedupe = this.readFromCachePromises.get(cord); + if (dedupe) return dedupe; + + // add to read queue + const promise = createDefer(); + this.readFromCachePromises.set(cord, promise); + + this.readFromCacheThrottle(); + + return promise; + } + + private writeCacheQueue = new Map(); + private writeToCacheThrottle = _throttle(this.writeToCache, WRITE_CACHE_BATCH_TIME); + private async writeToCache() { + if (this.writeCacheQueue.size === 0) return; + + this.dbLog(`Writing ${this.writeCacheQueue.size} events to database`); + for (const [_, event] of this.writeCacheQueue) localRelay.publish(event); + this.writeCacheQueue.clear(); + } + private async saveToCache(cord: string, event: NostrEvent) { + this.writeCacheQueue.set(cord, event); + this.writeToCacheThrottle(); + } + + private requestEventFromRelays(relays: Iterable, kind: number, pubkey: string, d?: string) { + const cord = createCoordinate(kind, pubkey, d); + const sub = this.subjects.get(cord); + + for (const relay of relays) this.loaders.get(relay).requestEvent(kind, pubkey, d); + + return sub; + } + + requestEvent(relays: Iterable, kind: number, pubkey: string, d?: string, opts: RequestOptions = {}) { + const key = createCoordinate(kind, pubkey, d); + const sub = this.subjects.get(key); + + if (!sub.value) { + this.loadFromCache(key).then((loaded) => { + if (!loaded && !sub.value) this.requestEventFromRelays(relays, kind, pubkey, d); + }); + } + + if (opts?.alwaysRequest || (!sub.value && opts.ignoreCache)) { + this.requestEventFromRelays(relays, kind, pubkey, d); + } + + return sub; + } +} + +const replaceableEventsService = new ReplaceableEventsService(); + +if (import.meta.env.DEV) { + //@ts-ignore + window.replaceableEventsService = replaceableEventsService; +} + +export default replaceableEventsService; diff --git a/src/services/settings/app-settings.ts b/src/services/settings/app-settings.ts index 926e35dd9..46b26af9c 100644 --- a/src/services/settings/app-settings.ts +++ b/src/services/settings/app-settings.ts @@ -12,6 +12,7 @@ appSettings.subscribe((event) => { log(`Changed`, event); }); +let accountSub: ZenObservable.Subscription; accountService.current.subscribe(() => { const account = accountService.current.value; @@ -20,7 +21,7 @@ accountService.current.subscribe(() => { return; } - appSettings.disconnectAll(); + if (accountSub) accountSub.unsubscribe(); if (account.localSettings) { appSettings.next(account.localSettings); @@ -30,8 +31,8 @@ accountService.current.subscribe(() => { const subject = userAppSettings.requestAppSettings(account.pubkey, clientRelaysService.readRelays.value, { alwaysRequest: true, }); - appSettings.next(defaultSettings); - appSettings.connect(subject); + appSettings.next(subject.value || defaultSettings); + accountSub = subject.subscribe((s) => appSettings.next(s)); }); // clientRelaysService.relays.subscribe(() => { diff --git a/src/services/settings/migrations.ts b/src/services/settings/migrations.ts index 4082faea9..46e3bc612 100644 --- a/src/services/settings/migrations.ts +++ b/src/services/settings/migrations.ts @@ -33,34 +33,12 @@ export type AppSettingsV3 = Omit & { version: 3; quick export type AppSettingsV4 = Omit & { version: 4; loadOpenGraphData: boolean }; export type AppSettingsV5 = Omit & { version: 5; hideUsernames: boolean }; export type AppSettingsV6 = Omit & { version: 6; noteDifficulty: number | null }; -export type AppSettingsV7 = Omit & { version: 7; defaultRelays: string[] }; +export type AppSettingsV7 = Omit & { version: 7; autoDecryptDMs: boolean }; -export function isV0(settings: { version: number }): settings is AppSettingsV0 { - return settings.version === undefined || settings.version === 0; -} -export function isV1(settings: { version: number }): settings is AppSettingsV1 { - return settings.version === 1; -} -export function isV2(settings: { version: number }): settings is AppSettingsV2 { - return settings.version === 2; -} -export function isV3(settings: { version: number }): settings is AppSettingsV3 { - return settings.version === 3; -} -export function isV4(settings: { version: number }): settings is AppSettingsV4 { - return settings.version === 4; -} -export function isV5(settings: { version: number }): settings is AppSettingsV5 { - return settings.version === 5; -} -export function isV6(settings: { version: number }): settings is AppSettingsV6 { - return settings.version === 6; -} - -export type AppSettings = AppSettingsV6; +export type AppSettings = AppSettingsV7; export const defaultSettings: AppSettings = { - version: 6, + version: 7, theme: "default", colorMode: "system", defaultRelays: ["wss://relay.damus.io", "wss://nostr.wine", "wss://nos.lol", "wss://welcome.nostr.wine"], @@ -74,6 +52,8 @@ export const defaultSettings: AppSettings = { showSignatureVerification: false, noteDifficulty: null, + autoDecryptDMs: false, + quickReactions: ["🤙", "❤️", "🤣", "😍", "🔥"], autoPayWithWebLN: true, @@ -89,14 +69,7 @@ export const defaultSettings: AppSettings = { }; export function upgradeSettings(settings: { version: number }): AppSettings | null { - if (isV0(settings)) return { ...defaultSettings, ...settings, version: 6 }; - if (isV1(settings)) return { ...defaultSettings, ...settings, version: 6 }; - if (isV2(settings)) return { ...defaultSettings, ...settings, version: 6 }; - if (isV3(settings)) return { ...defaultSettings, ...settings, version: 6 }; - if (isV4(settings)) return { ...defaultSettings, ...settings, version: 6 }; - if (isV5(settings)) return { ...defaultSettings, ...settings, version: 6 }; - if (isV6(settings)) return settings; - return null; + return { ...defaultSettings, ...settings, version: 7 }; } export function parseAppSettings(event: NostrEvent): AppSettings { diff --git a/src/services/settings/user-app-settings.ts b/src/services/settings/user-app-settings.ts index 9fee29c98..0a594b9af 100644 --- a/src/services/settings/user-app-settings.ts +++ b/src/services/settings/user-app-settings.ts @@ -5,7 +5,7 @@ import { DraftNostrEvent } from "../../types/nostr-event"; import SuperMap from "../../classes/super-map"; import { PersistentSubject } from "../../classes/subject"; import { AppSettings, defaultSettings, parseAppSettings } from "./migrations"; -import replaceableEventLoaderService, { RequestOptions } from "../replaceable-event-requester"; +import replaceableEventsService, { RequestOptions } from "../replaceable-events"; export const APP_SETTINGS_KIND = 30078; export const SETTING_EVENT_IDENTIFIER = "nostrudel-settings"; @@ -19,14 +19,14 @@ class UserAppSettings { } requestAppSettings(pubkey: string, relays: Iterable, opts?: RequestOptions) { const sub = this.parsedSubjects.get(pubkey); - const requestSub = replaceableEventLoaderService.requestEvent( + const requestSub = replaceableEventsService.requestEvent( relays, APP_SETTINGS_KIND, pubkey, SETTING_EVENT_IDENTIFIER, opts, ); - sub.connectWithHandler(requestSub, (event, next) => next(parseAppSettings(event))); + sub.connectWithMapper(requestSub, (event, next) => next(parseAppSettings(event))); return sub; } diff --git a/src/services/signing.tsx b/src/services/signing.tsx index 9d9741951..3bd188646 100644 --- a/src/services/signing.tsx +++ b/src/services/signing.tsx @@ -8,7 +8,7 @@ import amberSignerService from "./amber-signer"; import nostrConnectService from "./nostr-connect"; import { hexToBytes } from "@noble/hashes/utils"; -const decryptedKeys = new Map(); +const decryptedKeys = new Map>(); class SigningService { private async getSalt() { @@ -28,7 +28,10 @@ class SigningService { ); if (!password) throw new Error("Password required"); const enc = new TextEncoder(); - return window.crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"]); + return await window.crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, [ + "deriveBits", + "deriveKey", + ]); } private async getEncryptionKey() { const salt = await this.getSalt(); @@ -67,19 +70,29 @@ class SigningService { if (account.type !== "local") throw new Error("Account dose not have a secret key"); const cache = decryptedKeys.get(account.pubkey); - if (cache) return cache; + if (cache) return await cache; - const key = await this.getEncryptionKey(); - const decode = new TextDecoder(); + // create a promise to decrypt the key + const p = (async () => { + const key = await this.getEncryptionKey(); + const decode = new TextDecoder(); - try { - const decrypted = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: account.iv }, key, account.secKey); - const secKey = decode.decode(decrypted); - decryptedKeys.set(account.pubkey, secKey); - return secKey; - } catch (e) { - throw new Error("Failed to decrypt secret key"); - } + try { + const decrypted = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: account.iv }, key, account.secKey); + const secKey = decode.decode(decrypted); + decryptedKeys.set(account.pubkey, secKey); + return secKey; + } catch (e) { + console.log(e); + throw new Error("Failed to decrypt secret key"); + } + })(); + + // cache the promise so its only called once + decryptedKeys.set(account.pubkey, p); + + // await, return key + return await p; } async requestSignature(draft: DraftNostrEvent, account: Account) { @@ -181,7 +194,7 @@ class SigningService { await client.ensureConnected(); return await client.nip04Encrypt(pubkey, text); default: - throw new Error("Unknown connection type"); + throw new Error("Unknown account type"); } } } diff --git a/src/services/single-event.ts b/src/services/single-event.ts index 7d2847f7a..79df75e4a 100644 --- a/src/services/single-event.ts +++ b/src/services/single-event.ts @@ -1,22 +1,22 @@ import _throttle from "lodash.throttle"; import NostrRequest from "../classes/nostr-request"; -import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; import { localRelay } from "./local-relay"; import { relayRequest, safeRelayUrls } from "../helpers/relay"; import { logger } from "../helpers/debug"; +import Subject from "../classes/subject"; const RELAY_REQUEST_BATCH_TIME = 500; class SingleEventService { - private cache = new SuperMap>(() => new Subject()); + private subjects = new SuperMap>(() => new Subject()); pending = new Map(); log = logger.extend("SingleEvent"); requestEvent(id: string, relays: Iterable) { - const subject = this.cache.get(id); + const subject = this.subjects.get(id); if (subject.value) return subject; const safeURLs = safeRelayUrls(Array.from(relays)); @@ -28,8 +28,7 @@ class SingleEventService { } handleEvent(event: NostrEvent, cache = true) { - this.cache.get(event.id).next(event); - + this.subjects.get(event.id).next(event); if (cache) localRelay.publish(event); } @@ -62,7 +61,7 @@ class SingleEventService { for (const [relay, ids] of Object.entries(idsFromRelays)) { const request = new NostrRequest([relay]); - request.onEvent.subscribe(this.handleEvent, this); + request.onEvent.subscribe((event) => this.handleEvent(event)); request.start({ ids }); } this.pending.clear(); @@ -71,4 +70,9 @@ class SingleEventService { const singleEventService = new SingleEventService(); +if (import.meta.env.DEV) { + //@ts-expect-error + window.singleEventService = singleEventService; +} + export default singleEventService; diff --git a/src/services/user-event-sync.ts b/src/services/user-event-sync.ts index 3debdfa36..0ba674edc 100644 --- a/src/services/user-event-sync.ts +++ b/src/services/user-event-sync.ts @@ -4,7 +4,7 @@ import { logger } from "../helpers/debug"; import accountService from "./account"; import clientRelaysService from "./client-relays"; import { offlineMode } from "./offline-mode"; -import replaceableEventLoaderService from "./replaceable-event-requester"; +import replaceableEventsService from "./replaceable-events"; import userAppSettings from "./settings/user-app-settings"; import userMailboxesService from "./user-mailboxes"; import userMetadataService from "./user-metadata"; @@ -15,7 +15,7 @@ function loadContactsList() { const account = accountService.current.value!; log("Loading contacts list"); - replaceableEventLoaderService.requestEvent( + replaceableEventsService.requestEvent( [...clientRelaysService.readRelays.value, COMMON_CONTACT_RELAY], kinds.Contacts, account.pubkey, diff --git a/src/services/user-mailboxes.ts b/src/services/user-mailboxes.ts index 61c99174d..d7e14f7d6 100644 --- a/src/services/user-mailboxes.ts +++ b/src/services/user-mailboxes.ts @@ -3,10 +3,11 @@ import { kinds } from "nostr-tools"; import { NostrEvent } from "../types/nostr-event"; import SuperMap from "../classes/super-map"; import Subject from "../classes/subject"; -import replaceableEventLoaderService, { createCoordinate, RequestOptions } from "./replaceable-event-requester"; +import replaceableEventsService, { RequestOptions } from "./replaceable-events"; import RelaySet from "../classes/relay-set"; import { RelayMode } from "../classes/relay"; import { relaysFromContactsEvent } from "../helpers/nostr/contacts"; +import { createCoordinate } from "../classes/batch-kind-loader"; export type UserMailboxes = { pubkey: string; @@ -29,18 +30,19 @@ function nip65ToUserMailboxes(event: NostrEvent): UserMailboxes { } class UserMailboxesService { - private subjects = new SuperMap>(() => new Subject()); + private subjects = new SuperMap>((pubkey) => + replaceableEventsService.getEvent(kinds.RelayList, pubkey).map(nip65ToUserMailboxes), + ); getMailboxes(pubkey: string) { return this.subjects.get(pubkey); } requestMailboxes(pubkey: string, relays: Iterable, opts: RequestOptions = {}) { const sub = this.subjects.get(pubkey); - const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.RelayList, pubkey, undefined, opts); - sub.connectWithHandler(requestSub, (event, next) => next(nip65ToUserMailboxes(event))); + replaceableEventsService.requestEvent(relays, kinds.RelayList, pubkey, undefined, opts); // also fetch the relays from the users contacts - const contactsSub = replaceableEventLoaderService.requestEvent(relays, kinds.Contacts, pubkey, undefined, opts); - sub.connectWithHandler(contactsSub, (event, next, value) => { + const contactsSub = replaceableEventsService.requestEvent(relays, kinds.Contacts, pubkey, undefined, opts); + sub.connectWithMapper(contactsSub, (event, next, value) => { // NOTE: only use relays from contact list if the user dose not have a NIP-65 relay list const relays = relaysFromContactsEvent(event); if (relays.length > 0 && !value) { @@ -60,16 +62,12 @@ class UserMailboxesService { async loadFromCache(pubkey: string) { const sub = this.subjects.get(pubkey); - - // load from cache - await replaceableEventLoaderService.loadFromCache(createCoordinate(kinds.RelayList, pubkey)); - - const requestSub = replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey); - sub.connectWithHandler(requestSub, (event, next) => next(nip65ToUserMailboxes(event))); + await replaceableEventsService.loadFromCache(createCoordinate(kinds.RelayList, pubkey)); + return sub; } receiveEvent(event: NostrEvent) { - replaceableEventLoaderService.handleEvent(event); + replaceableEventsService.handleEvent(event); } } diff --git a/src/services/user-metadata.ts b/src/services/user-metadata.ts index 798ee48fd..ce3fc7fba 100644 --- a/src/services/user-metadata.ts +++ b/src/services/user-metadata.ts @@ -1,53 +1,22 @@ -import db from "./db"; import { kinds } from "nostr-tools"; import _throttle from "lodash.throttle"; -import { Kind0ParsedContent, getSearchNames, parseKind0Event } from "../helpers/user-metadata"; +import { Kind0ParsedContent, parseMetadataContent } from "../helpers/nostr/user-metadata"; import SuperMap from "../classes/super-map"; import Subject from "../classes/subject"; -import replaceableEventLoaderService, { RequestOptions } from "./replaceable-event-requester"; - -const WRITE_USER_SEARCH_BATCH_TIME = 500; +import replaceableEventsService, { RequestOptions } from "./replaceable-events"; class UserMetadataService { private metadata = new SuperMap>((pubkey) => { - const sub = new Subject(); - sub.subscribe((metadata) => { - if (metadata) { - this.writeSearchQueue.add(pubkey); - this.writeSearchDataThrottle(); - } - }); - return sub; + return replaceableEventsService.getEvent(0, pubkey).map(parseMetadataContent); }); getSubject(pubkey: string) { return this.metadata.get(pubkey); } requestMetadata(pubkey: string, relays: Iterable, opts: RequestOptions = {}) { - const sub = this.metadata.get(pubkey); - const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.Metadata, pubkey, undefined, opts); - sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event))); - return sub; - } - - private writeSearchQueue = new Set(); - private writeSearchDataThrottle = _throttle(this.writeSearchData.bind(this), WRITE_USER_SEARCH_BATCH_TIME); - private async writeSearchData() { - if (this.writeSearchQueue.size === 0) return; - - const keys = Array.from(this.writeSearchQueue); - this.writeSearchQueue.clear(); - - const transaction = db.transaction("userSearch", "readwrite"); - for (const pubkey of keys) { - const metadata = this.getSubject(pubkey).value; - if (metadata) { - const names = getSearchNames(metadata); - transaction.objectStore("userSearch").put({ pubkey, names }); - } - } - transaction.commit(); - await transaction.done; + const subject = this.metadata.get(pubkey); + replaceableEventsService.requestEvent(relays, kinds.Metadata, pubkey, undefined, opts); + return subject; } } diff --git a/src/services/username-search.ts b/src/services/username-search.ts new file mode 100644 index 000000000..52eff53a2 --- /dev/null +++ b/src/services/username-search.ts @@ -0,0 +1,36 @@ +import _throttle from "lodash.throttle"; +import { getSearchNames } from "../helpers/nostr/user-metadata"; +import db from "./db"; +import replaceableEventsService from "./replaceable-events"; +import userMetadataService from "./user-metadata"; +import { logger } from "../helpers/debug"; + +const WRITE_USER_SEARCH_BATCH_TIME = 500; +const log = logger.extend("UsernameSearch"); + +const writeSearchQueue = new Set(); +const writeSearchData = _throttle(async () => { + if (writeSearchQueue.size === 0) return; + + log(`Writing ${writeSearchQueue.size} to search table`); + const keys = Array.from(writeSearchQueue); + writeSearchQueue.clear(); + + const transaction = db.transaction("userSearch", "readwrite"); + for (const pubkey of keys) { + const metadata = userMetadataService.getSubject(pubkey).value; + if (metadata) { + const names = getSearchNames(metadata); + transaction.objectStore("userSearch").put({ pubkey, names }); + } + } + transaction.commit(); + await transaction.done; +}, WRITE_USER_SEARCH_BATCH_TIME); + +replaceableEventsService.events.onEvent.subscribe((event) => { + if (event.kind === 0) { + writeSearchQueue.add(event.pubkey); + writeSearchData(); + } +}); diff --git a/src/types/emojilib.d.ts b/src/types/emojilib.d.ts index f152a47e6..57ececd62 100644 --- a/src/types/emojilib.d.ts +++ b/src/types/emojilib.d.ts @@ -1,11 +1,4 @@ -type EmojiShape = { - keywords: string[]; - char: string; - fitzpatrick_scale: boolean; - category: string; -}; - declare module "emojilib" { - const lib: { [key: string]: EmojiShape }; - export { lib }; + const lib: { [char: string]: string[] }; + export default lib; } diff --git a/src/types/nostr-event.ts b/src/types/nostr-event.ts index 2d77d9563..307f0acb6 100644 --- a/src/types/nostr-event.ts +++ b/src/types/nostr-event.ts @@ -12,25 +12,9 @@ export type Tag = string[] | ETag | PTag | RTag | DTag | ATag | ExpirationTag; export type NostrEvent = Omit & { tags: Tag[]; }; -export type CountResponse = { - count: number; - approximate?: boolean; -}; export type DraftNostrEvent = Omit & { pubkey?: string; id?: string }; -export type RawIncomingEvent = ["EVENT", string, NostrEvent]; -export type RawIncomingNotice = ["NOTICE", string]; -export type RawIncomingCount = ["COUNT", string, CountResponse]; -export type RawIncomingEOSE = ["EOSE", string]; -export type RawIncomingCommandResult = ["OK", string, boolean, string]; -export type RawIncomingNostrEvent = - | RawIncomingEvent - | RawIncomingNotice - | RawIncomingCount - | RawIncomingEOSE - | RawIncomingCommandResult; - export function isETag(tag: Tag): tag is ETag { return tag[0] === "e" && tag[1] !== undefined; } diff --git a/src/types/nostr-query.ts b/src/types/nostr-query.ts deleted file mode 100644 index 11755efaf..000000000 --- a/src/types/nostr-query.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Filter } from "nostr-tools"; -import { NostrEvent } from "./nostr-event"; - -export type NostrOutgoingEvent = ["EVENT", NostrEvent]; -export type NostrOutgoingRequest = ["REQ", string, ...Filter[]]; -export type NostrOutgoingCount = ["COUNT", string, ...Filter[]]; -export type NostrOutgoingClose = ["CLOSE", string]; - -export type NostrOutgoingMessage = NostrOutgoingEvent | NostrOutgoingRequest | NostrOutgoingClose | NostrOutgoingCount; - -/** @deprecated use Filter instead */ -export type NostrQuery = Filter; - -export type NostrRequestFilter = Filter | Filter[]; - -export type RelayQueryMap = Record; diff --git a/src/types/nostr-relay.ts b/src/types/nostr-relay.ts new file mode 100644 index 000000000..ed6c89d3e --- /dev/null +++ b/src/types/nostr-relay.ts @@ -0,0 +1,8 @@ +import { Filter } from "nostr-tools"; + +/** @deprecated use Filter instead */ +export type NostrQuery = Filter; + +export type NostrRequestFilter = Filter | Filter[]; + +export type RelayQueryMap = Record; diff --git a/src/views/badges/badge-details.tsx b/src/views/badges/badge-details.tsx index 6307368f5..eea82a7ac 100644 --- a/src/views/badges/badge-details.tsx +++ b/src/views/badges/badge-details.tsx @@ -18,7 +18,6 @@ import { import { ChevronLeftIcon } from "../../components/icons"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; -import { EventRelays } from "../../components/note/note-relays"; import { getBadgeAwardPubkeys, getBadgeDescription, getBadgeImage, getBadgeName } from "../../helpers/nostr/badges"; import BadgeMenu from "./components/badge-menu"; import useTimelineLoader from "../../hooks/use-timeline-loader"; @@ -27,15 +26,16 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useSubject from "../../hooks/use-subject"; import { NostrEvent } from "../../types/nostr-event"; -import { getEventCoordinate } from "../../helpers/nostr/events"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import { getEventCoordinate } from "../../helpers/nostr/event"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import Timestamp from "../../components/timestamp"; import VerticalPageLayout from "../../components/vertical-page-layout"; import BadgeAwardCard from "./components/badge-award-card"; import TimelineLoader from "../../classes/timeline-loader"; import { ErrorBoundary } from "../../components/error-boundary"; import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; +import { EventRelays } from "../../components/note/event-relays"; function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) { const awards = useSubject(timeline.timeline); diff --git a/src/views/badges/browse.tsx b/src/views/badges/browse.tsx index 1b3fe45c2..2e56ff5ed 100644 --- a/src/views/badges/browse.tsx +++ b/src/views/badges/browse.tsx @@ -8,7 +8,7 @@ import { useReadRelays } from "../../hooks/use-client-relays"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useSubject from "../../hooks/use-subject"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import BadgeCard from "./components/badge-card"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/badges/components/badge-award-card.tsx b/src/views/badges/components/badge-award-card.tsx index eaee74962..9faa5c08a 100644 --- a/src/views/badges/components/badge-award-card.tsx +++ b/src/views/badges/components/badge-award-card.tsx @@ -6,11 +6,11 @@ import { getBadgeAwardBadge, getBadgeAwardPubkeys, getBadgeImage, getBadgeName } import useReplaceableEvent from "../../../hooks/use-replaceable-event"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getSharableEventAddress } from "../../../helpers/nip19"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; import Timestamp from "../../../components/timestamp"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; const UserCard = memo(({ pubkey }: { pubkey: string }) => ( diff --git a/src/views/badges/components/badge-card.tsx b/src/views/badges/components/badge-card.tsx index d17b6ce45..624b67fd4 100644 --- a/src/views/badges/components/badge-card.tsx +++ b/src/views/badges/components/badge-card.tsx @@ -3,12 +3,12 @@ import { Link as RouterLink, useNavigate } from "react-router-dom"; import { ButtonGroup, Card, CardBody, CardHeader, CardProps, Flex, Heading, Image, Link, Text } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { getSharableEventAddress } from "../../../helpers/nip19"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/event"; import BadgeMenu from "./badge-menu"; import { getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges"; import Timestamp from "../../../components/timestamp"; diff --git a/src/views/badges/components/badge-menu.tsx b/src/views/badges/components/badge-menu.tsx index 00e609450..9e69a1a39 100644 --- a/src/views/badges/components/badge-menu.tsx +++ b/src/views/badges/components/badge-menu.tsx @@ -1,7 +1,7 @@ import { MenuItem } from "@chakra-ui/react"; import { NostrEvent } from "../../../types/nostr-event"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import useCurrentAccount from "../../../hooks/use-current-account"; import { TrashIcon } from "../../../components/icons"; import { useDeleteEventContext } from "../../../providers/route/delete-event-provider"; @@ -16,7 +16,7 @@ export default function BadgeMenu({ badge, ...props }: { badge: NostrEvent } & O return ( <> - + {account?.pubkey === badge.pubkey && ( @@ -25,7 +25,7 @@ export default function BadgeMenu({ badge, ...props }: { badge: NostrEvent } & O )} - + ); } diff --git a/src/views/bookmarks/index.tsx b/src/views/bookmarks/index.tsx index 415b130cc..b50ddfba5 100644 --- a/src/views/bookmarks/index.tsx +++ b/src/views/bookmarks/index.tsx @@ -7,15 +7,15 @@ import useCurrentAccount from "../../hooks/use-current-account"; import TimelineItem from "../../components/timeline-page/generic-note-timeline/timeline-item"; import useSingleEvent from "../../hooks/use-single-event"; import userUserBookmarksList from "../../hooks/use-user-bookmarks-list"; -import UserName from "../../components/user-name"; +import UserName from "../../components/user/user-name"; import ListMenu from "../lists/components/list-menu"; -import UserAvatarLink from "../../components/user-avatar-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; import { NostrEvent, isATag, isETag } from "../../types/nostr-event"; import useEventBookmarkActions from "../../hooks/use-event-bookmark-actions"; import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; import { EmbedEvent } from "../../components/embed-event"; -import { aTagToAddressPointer, eTagToEventPointer } from "../../helpers/nostr/events"; +import { aTagToAddressPointer, eTagToEventPointer } from "../../helpers/nostr/event"; function RemoveBookmarkButton({ event }: { event: NostrEvent }) { const { isLoading, removeBookmark } = useEventBookmarkActions(event); diff --git a/src/views/channels/components/channel-card.tsx b/src/views/channels/components/channel-card.tsx index fb61a5965..8cf5dfbca 100644 --- a/src/views/channels/components/channel-card.tsx +++ b/src/views/channels/components/channel-card.tsx @@ -20,8 +20,8 @@ import useChannelMetadata from "../../../hooks/use-channel-metadata"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import useSingleEvent from "../../../hooks/use-single-event"; import { useReadRelays } from "../../../hooks/use-client-relays"; import singleEventService from "../../../services/single-event"; diff --git a/src/views/channels/components/channel-menu.tsx b/src/views/channels/components/channel-menu.tsx index 155e47e64..9d31c20e1 100644 --- a/src/views/channels/components/channel-menu.tsx +++ b/src/views/channels/components/channel-menu.tsx @@ -1,4 +1,4 @@ -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { NostrEvent } from "../../../types/nostr-event"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; @@ -10,11 +10,11 @@ export default function ChannelMenu({ }: Omit & { channel: NostrEvent }) { return ( <> - + - + ); } diff --git a/src/views/channels/components/channel-message-block.tsx b/src/views/channels/components/channel-message-block.tsx index 60d15e31b..ff7f3e7b9 100644 --- a/src/views/channels/components/channel-message-block.tsx +++ b/src/views/channels/components/channel-message-block.tsx @@ -1,7 +1,7 @@ import { ReactNode, memo, useCallback } from "react"; import { NostrEvent } from "../../../types/nostr-event"; -import MessageBlock, { MessageBlockProps } from "../../../components/message-block"; +import MessageBlock, { MessageBlockProps } from "../../../components/message/message-block"; import ChannelMessageContent from "./channel-message-content"; function ChannelMessageBlock({ ...props }: Omit) { diff --git a/src/views/channels/components/channel-metadata-drawer.tsx b/src/views/channels/components/channel-metadata-drawer.tsx index 632d0621e..e0682b621 100644 --- a/src/views/channels/components/channel-metadata-drawer.tsx +++ b/src/views/channels/components/channel-metadata-drawer.tsx @@ -22,10 +22,10 @@ import useTimelineLoader from "../../../hooks/use-timeline-loader"; import useSubject from "../../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import UserAvatar from "../../../components/user-avatar"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import UserAvatar from "../../../components/user/user-avatar"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import ChannelJoinButton from "./channel-join-button"; import { ExternalLinkIcon } from "../../../components/icons"; import { CHANNELS_LIST_KIND } from "../../../helpers/nostr/lists"; diff --git a/src/views/communities/components/community-card.tsx b/src/views/communities/components/community-card.tsx index 3a92ed003..da2a6337a 100644 --- a/src/views/communities/components/community-card.tsx +++ b/src/views/communities/components/community-card.tsx @@ -17,10 +17,10 @@ import { import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import useCountCommunityMembers from "../../../hooks/use-count-community-members"; import { readablizeSats } from "../../../helpers/bolt11"; import User01 from "../../../components/icons/user-01"; diff --git a/src/views/communities/components/community-create-modal.tsx b/src/views/communities/components/community-create-modal.tsx index a53bddc97..7493692d7 100644 --- a/src/views/communities/components/community-create-modal.tsx +++ b/src/views/communities/components/community-create-modal.tsx @@ -26,8 +26,8 @@ import { import { SubmitHandler, useForm } from "react-hook-form"; import useCurrentAccount from "../../../hooks/use-current-account"; -import UserAvatar from "../../../components/user-avatar"; -import UserLink from "../../../components/user-link"; +import UserAvatar from "../../../components/user/user-avatar"; +import UserLink from "../../../components/user/user-link"; import { TrashIcon } from "../../../components/icons"; import Upload01 from "../../../components/icons/upload-01"; import { nostrBuildUploadImage } from "../../../helpers/nostr-build"; diff --git a/src/views/communities/components/community-join-button.tsx b/src/views/communities/components/community-join-button.tsx index f4595a4f2..3530b28ed 100644 --- a/src/views/communities/components/community-join-button.tsx +++ b/src/views/communities/components/community-join-button.tsx @@ -7,7 +7,7 @@ import useUserCommunitiesList from "../../../hooks/use-user-communities-list"; import useCurrentAccount from "../../../hooks/use-current-account"; import { getCommunityName } from "../../../helpers/nostr/communities"; import { COMMUNITIES_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import { usePublishEvent } from "../../../providers/global/publish-provider"; export default function CommunityJoinButton({ diff --git a/src/views/communities/components/community-mod-list.tsx b/src/views/communities/components/community-mod-list.tsx index bd1a908b5..1e72b54b8 100644 --- a/src/views/communities/components/community-mod-list.tsx +++ b/src/views/communities/components/community-mod-list.tsx @@ -1,6 +1,6 @@ import { AvatarGroup, AvatarGroupProps } from "@chakra-ui/react"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; import { NostrEvent } from "../../../types/nostr-event"; import { getCommunityMods } from "../../../helpers/nostr/communities"; diff --git a/src/views/communities/explore.tsx b/src/views/communities/explore.tsx index ca1c7e079..6d09b5e9b 100644 --- a/src/views/communities/explore.tsx +++ b/src/views/communities/explore.tsx @@ -9,20 +9,18 @@ import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; import { ErrorBoundary } from "../../components/error-boundary"; import { useReadRelays } from "../../hooks/use-client-relays"; import useSubjects from "../../hooks/use-subjects"; -import replaceableEventLoaderService from "../../services/replaceable-event-requester"; +import replaceableEventsService from "../../services/replaceable-events"; import { COMMUNITIES_LIST_KIND, getCoordinatesFromList } from "../../helpers/nostr/lists"; import { useNavigate } from "react-router-dom"; import { ChevronLeftIcon } from "../../components/icons"; -import { parseCoordinate } from "../../helpers/nostr/events"; -import UserAvatarLink from "../../components/user-avatar-link"; +import { parseCoordinate } from "../../helpers/nostr/event"; +import UserAvatarLink from "../../components/user/user-avatar-link"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays?: Iterable) { const readRelays = useReadRelays(additionalRelays); const communityListsSubjects = useMemo(() => { - return pubkeys.map((pubkey) => - replaceableEventLoaderService.requestEvent(readRelays, COMMUNITIES_LIST_KIND, pubkey), - ); + return pubkeys.map((pubkey) => replaceableEventsService.requestEvent(readRelays, COMMUNITIES_LIST_KIND, pubkey)); }, [pubkeys]); return useSubjects(communityListsSubjects); } diff --git a/src/views/communities/hooks/use-count-community-post.ts b/src/views/communities/hooks/use-count-community-post.ts index 644c59101..bd225cad4 100644 --- a/src/views/communities/hooks/use-count-community-post.ts +++ b/src/views/communities/hooks/use-count-community-post.ts @@ -3,7 +3,7 @@ import { kinds } from "nostr-tools"; import { NostrEvent } from "../../../types/nostr-event"; import useEventCount from "../../../hooks/use-event-count"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; export default function useCountCommunityPosts( community: NostrEvent, diff --git a/src/views/communities/index.tsx b/src/views/communities/index.tsx index 093473bf3..a628f5b7d 100644 --- a/src/views/communities/index.tsx +++ b/src/views/communities/index.tsx @@ -35,7 +35,6 @@ import { getCommunityMods, getCommunityName, } from "../../helpers/nostr/communities"; -import { createCoordinate } from "../../services/replaceable-event-requester"; import { getImageSize } from "../../helpers/image"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; @@ -43,11 +42,12 @@ import useSubject from "../../hooks/use-subject"; import useUserMuteFilter from "../../hooks/use-user-mute-filter"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useReplaceableEvents from "../../hooks/use-replaceable-events"; -import { getEventCoordinate, sortByDate } from "../../helpers/nostr/events"; +import { getEventCoordinate, sortByDate } from "../../helpers/nostr/event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import ApprovedEvent from "../community/components/community-approved-post"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import { usePublishEvent } from "../../providers/global/publish-provider"; +import { createCoordinate } from "../../classes/batch-kind-loader"; function CommunitiesHomePage() { const publish = usePublishEvent(); diff --git a/src/views/community/community-home.tsx b/src/views/community/community-home.tsx index bfa4d0353..7314cdc2a 100644 --- a/src/views/community/community-home.tsx +++ b/src/views/community/community-home.tsx @@ -13,8 +13,8 @@ import { } from "../../helpers/nostr/communities"; import { NostrEvent } from "../../types/nostr-event"; import VerticalPageLayout from "../../components/vertical-page-layout"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import { AdditionalRelayProvider } from "../../providers/local/additional-relay-context"; import TrendUp01 from "../../components/icons/trend-up-01"; @@ -25,7 +25,7 @@ import { useBreakpointValue } from "../../providers/global/breakpoint-provider"; import HorizontalCommunityDetails from "./components/horizonal-community-details"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; -import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID } from "../../helpers/nostr/event"; import { WritingIcon } from "../../components/icons"; import { PostModalContext } from "../../providers/route/post-modal-provider"; import CommunityEditModal from "./components/community-edit-modal"; diff --git a/src/views/community/components/community-members-modal.tsx b/src/views/community/components/community-members-modal.tsx index 513a368b4..ffa40accb 100644 --- a/src/views/community/components/community-members-modal.tsx +++ b/src/views/community/components/community-members-modal.tsx @@ -16,15 +16,15 @@ import { NostrEvent } from "../../../types/nostr-event"; import useTimelineLoader from "../../../hooks/use-timeline-loader"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { getCommunityRelays } from "../../../helpers/nostr/communities"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import { COMMUNITIES_LIST_KIND } from "../../../helpers/nostr/lists"; import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; import useSubject from "../../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status"; -import UserLink from "../../../components/user-link"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; function UserCard({ pubkey }: { pubkey: string }) { return ( diff --git a/src/views/community/components/community-menu.tsx b/src/views/community/components/community-menu.tsx index 4cfdd49b9..16b8dbda6 100644 --- a/src/views/community/components/community-menu.tsx +++ b/src/views/community/components/community-menu.tsx @@ -1,6 +1,6 @@ import { MenuItem } from "@chakra-ui/react"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { NostrEvent } from "../../../types/nostr-event"; import useCurrentAccount from "../../../hooks/use-current-account"; import PencilLine from "../../../components/icons/pencil-line"; @@ -17,7 +17,7 @@ export default function CommunityMenu({ return ( <> - + {account?.pubkey === community.pubkey && onEditClick && ( @@ -26,7 +26,7 @@ export default function CommunityMenu({ )} - + ); } diff --git a/src/views/community/components/community-post-menu.tsx b/src/views/community/components/community-post-menu.tsx index 4cea5cd99..547c6b7fc 100644 --- a/src/views/community/components/community-post-menu.tsx +++ b/src/views/community/components/community-post-menu.tsx @@ -1,7 +1,7 @@ import { MenuItem, useToast } from "@chakra-ui/react"; import { nip19 } from "nostr-tools"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { NostrEvent } from "../../../types/nostr-event"; import { CopyToClipboardIcon } from "../../../components/icons"; import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; @@ -18,7 +18,7 @@ export default function CommunityPostMenu({ return ( <> - + - + ); } diff --git a/src/views/community/components/community-post.tsx b/src/views/community/components/community-post.tsx index 0495ddd9c..2c4ca9012 100644 --- a/src/views/community/components/community-post.tsx +++ b/src/views/community/components/community-post.tsx @@ -23,9 +23,9 @@ import { getSharableEventAddress } from "../../../helpers/nip19"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; import { CompactNoteContent } from "../../../components/compact-note-content"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID, parseHardcodedNoteContent } from "../../../helpers/nostr/events"; -import UserLink from "../../../components/user-link"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import { getEventUID, parseHardcodedNoteContent } from "../../../helpers/nostr/event"; +import UserLink from "../../../components/user/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; import useUserMuteFilter from "../../../hooks/use-user-mute-filter"; import { useReadRelays } from "../../../hooks/use-client-relays"; import useSingleEvent from "../../../hooks/use-single-event"; diff --git a/src/views/community/components/horizonal-community-details.tsx b/src/views/community/components/horizonal-community-details.tsx index acfdcf1e9..a77c7524a 100644 --- a/src/views/community/components/horizonal-community-details.tsx +++ b/src/views/community/components/horizonal-community-details.tsx @@ -22,8 +22,8 @@ import { getCommunityRules, } from "../../../helpers/nostr/communities"; import CommunityDescription from "../../communities/components/community-description"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { NostrEvent } from "../../../types/nostr-event"; import CommunityJoinButton from "../../communities/components/community-join-button"; import CommunityMenu from "./community-menu"; diff --git a/src/views/community/components/vertical-community-details.tsx b/src/views/community/components/vertical-community-details.tsx index 2b23d9534..e4c8f1126 100644 --- a/src/views/community/components/vertical-community-details.tsx +++ b/src/views/community/components/vertical-community-details.tsx @@ -9,8 +9,8 @@ import { getCommunityRules, } from "../../../helpers/nostr/communities"; import CommunityDescription from "../../communities/components/community-description"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { NostrEvent } from "../../../types/nostr-event"; import CommunityJoinButton from "../../communities/components/community-join-button"; import CommunityMenu from "./community-menu"; diff --git a/src/views/community/find-by-name.tsx b/src/views/community/find-by-name.tsx index 1744802d9..ce12bff38 100644 --- a/src/views/community/find-by-name.tsx +++ b/src/views/community/find-by-name.tsx @@ -11,7 +11,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline- import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import VerticalPageLayout from "../../components/vertical-page-layout"; import CommunityCard from "../communities/components/community-card"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { safeDecode } from "../../helpers/nip19"; export default function CommunityFindByNameView() { diff --git a/src/views/community/views/pending.tsx b/src/views/community/views/pending.tsx index 0bf731245..5a4a069ab 100644 --- a/src/views/community/views/pending.tsx +++ b/src/views/community/views/pending.tsx @@ -4,7 +4,7 @@ import { useOutletContext } from "react-router-dom"; import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; -import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/event"; import { COMMUNITY_APPROVAL_KIND, buildApprovalMap, diff --git a/src/views/dms/chat.tsx b/src/views/dms/chat.tsx index 2cb2feb0a..2494b5dc8 100644 --- a/src/views/dms/chat.tsx +++ b/src/views/dms/chat.tsx @@ -4,8 +4,8 @@ import { UNSAFE_DataRouterContext, useLocation, useNavigate } from "react-router import { kinds } from "nostr-tools"; import { ChevronLeftIcon, ThreadIcon } from "../../components/icons"; -import UserAvatar from "../../components/user-avatar"; -import UserLink from "../../components/user-link"; +import UserAvatar from "../../components/user/user-avatar"; +import UserLink from "../../components/user/user-link"; import useSubject from "../../hooks/use-subject"; import RequireCurrentAccount from "../../providers/route/require-current-account"; import useTimelineLoader from "../../hooks/use-timeline-loader"; @@ -13,7 +13,7 @@ import useCurrentAccount from "../../hooks/use-current-account"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; -import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; +import { UserDnsIdentityIcon } from "../../components/user/user-dns-identity-icon"; import { useDecryptionContext } from "../../providers/global/dycryption-provider"; import SendMessageForm from "./components/send-message-form"; import { groupMessages } from "../../helpers/nostr/dms"; @@ -25,6 +25,7 @@ import DirectMessageBlock from "./components/direct-message-block"; import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer"; import useUserMailboxes from "../../hooks/use-user-mailboxes"; import RelaySet from "../../classes/relay-set"; +import useAppSettings from "../../hooks/use-app-settings"; /** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */ const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => { @@ -46,6 +47,7 @@ const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => { function DirectMessageChatPage({ pubkey }: { pubkey: string }) { const account = useCurrentAccount()!; + const { autoDecryptDMs } = useAppSettings(); const navigate = useNavigate(); const location = useLocation(); const { getOrCreateContainer, addToQueue, startQueue } = useDecryptionContext(); @@ -124,9 +126,11 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) { - + {!autoDecryptDMs && ( + + )} ): JSX.Element { const account = useCurrentAccount(); + const { autoDecryptDMs } = useAppSettings(); const isOwn = account?.pubkey === message.pubkey; const [loading, setLoading] = useState(false); const { requestDecrypt, plaintext, error } = useDecryptionContainer( @@ -32,6 +34,18 @@ export default function DecryptPlaceholder({ setLoading(false); }; + // auto decrypt + useEffect(() => { + if (autoDecryptDMs && !plaintext && !error) { + setLoading(true); + requestDecrypt() + .catch(() => {}) + .finally(() => { + setLoading(false); + }); + } + }, [autoDecryptDMs, error, plaintext]); + if (plaintext) { return children(plaintext); } diff --git a/src/views/dms/components/direct-message-block.tsx b/src/views/dms/components/direct-message-block.tsx index b779309ae..adce2abae 100644 --- a/src/views/dms/components/direct-message-block.tsx +++ b/src/views/dms/components/direct-message-block.tsx @@ -1,7 +1,7 @@ import { ReactNode, memo, useCallback } from "react"; import { NostrEvent } from "../../../types/nostr-event"; -import MessageBlock, { MessageBlockProps } from "../../../components/message-block"; +import MessageBlock, { MessageBlockProps } from "../../../components/message/message-block"; import DecryptPlaceholder from "./decrypt-placeholder"; import DirectMessageContent from "./direct-message-content"; diff --git a/src/views/dms/components/thread-button.tsx b/src/views/dms/components/thread-button.tsx index 0fbe4219c..918ce2475 100644 --- a/src/views/dms/components/thread-button.tsx +++ b/src/views/dms/components/thread-button.tsx @@ -1,7 +1,7 @@ import { Button, IconButton } from "@chakra-ui/react"; import { useLocation, useNavigate } from "react-router-dom"; -import UserAvatar from "../../../components/user-avatar"; +import UserAvatar from "../../../components/user/user-avatar"; import { Thread } from "../../../providers/local/thread-provider"; import { ChevronRightIcon, ThreadIcon } from "../../../components/icons"; import { IconButtonProps } from "yet-another-react-lightbox"; diff --git a/src/views/dms/components/thread-drawer.tsx b/src/views/dms/components/thread-drawer.tsx index b8e39553c..991e63fb6 100644 --- a/src/views/dms/components/thread-drawer.tsx +++ b/src/views/dms/components/thread-drawer.tsx @@ -19,8 +19,8 @@ import { } from "@chakra-ui/react"; import { NostrEvent } from "../../../types/nostr-event"; -import UserAvatar from "../../../components/user-avatar"; -import UserLink from "../../../components/user-link"; +import UserAvatar from "../../../components/user/user-avatar"; +import UserLink from "../../../components/user/user-link"; import DecryptPlaceholder from "./decrypt-placeholder"; import Timestamp from "../../../components/timestamp"; import { Thread, useThreadsContext } from "../../../providers/local/thread-provider"; diff --git a/src/views/dms/index.tsx b/src/views/dms/index.tsx index ef0bd9857..fb5a4eb7d 100644 --- a/src/views/dms/index.tsx +++ b/src/views/dms/index.tsx @@ -3,7 +3,7 @@ import { Card, CardBody, Flex, LinkBox, LinkOverlay, Text } from "@chakra-ui/rea import { Outlet, Link as RouterLink, useLocation, useParams } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import UserAvatar from "../../components/user-avatar"; +import UserAvatar from "../../components/user/user-avatar"; import useSubject from "../../hooks/use-subject"; import RequireCurrentAccount from "../../providers/route/require-current-account"; import Timestamp from "../../components/timestamp"; @@ -17,11 +17,11 @@ import IntersectionObserverProvider, { import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import { useDMTimeline } from "../../providers/global/dm-timeline"; -import UserName from "../../components/user-name"; +import UserName from "../../components/user/user-name"; import { useDecryptionContainer } from "../../providers/global/dycryption-provider"; import { NostrEvent } from "../../types/nostr-event"; import { CheckIcon } from "../../components/icons"; -import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; +import { UserDnsIdentityIcon } from "../../components/user/user-dns-identity-icon"; function MessagePreview({ message, pubkey }: { message: NostrEvent; pubkey: string }) { const ref = useRef(null); diff --git a/src/views/dvm-feed/components/debug-chains.tsx b/src/views/dvm-feed/components/debug-chains.tsx index d1ea27a14..dfe528229 100644 --- a/src/views/dvm-feed/components/debug-chains.tsx +++ b/src/views/dvm-feed/components/debug-chains.tsx @@ -16,20 +16,20 @@ import { } from "@chakra-ui/react"; import { ChainedDVMJob, getEventIdsFromJobs, getRequestInput, getRequestRelays } from "../../../helpers/nostr/dvm"; import dayjs from "dayjs"; -import { truncatedId } from "../../../helpers/nostr/events"; +import { truncatedId } from "../../../helpers/nostr/event"; import { CopyIconButton } from "../../../components/copy-icon-button"; import { NostrEvent } from "../../../types/nostr-event"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; function JobResult({ result }: { result: NostrEvent }) { return ( <> - ID: {truncatedId(result.id)} + ID: {truncatedId(result.id)} Content: {result.content}{" "} - + ); @@ -38,7 +38,7 @@ function JobStatus({ status }: { status: NostrEvent }) { return ( <> - ID: {truncatedId(status.id)} + ID: {truncatedId(status.id)} Status: {status.tags.find((t) => t[0] === "status")?.[1]} Content: {status.content} @@ -54,7 +54,7 @@ function ChainedJob({ job }: { job: ChainedDVMJob }) { return ( - ID: {job.request.id} + ID: {job.request.id} {input && ( diff --git a/src/views/dvm-feed/components/dvm-avatar.tsx b/src/views/dvm-feed/components/dvm-avatar.tsx index ca2436d7f..9208d0bee 100644 --- a/src/views/dvm-feed/components/dvm-avatar.tsx +++ b/src/views/dvm-feed/components/dvm-avatar.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { nip19 } from "nostr-tools"; import { Box, BoxProps } from "@chakra-ui/react"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; import useDVMMetadata from "../../../hooks/use-dvm-metadata"; diff --git a/src/views/dvm-feed/components/dvm-card.tsx b/src/views/dvm-feed/components/dvm-card.tsx index 34e0edd4e..162a26045 100644 --- a/src/views/dvm-feed/components/dvm-card.tsx +++ b/src/views/dvm-feed/components/dvm-card.tsx @@ -5,7 +5,7 @@ import { useMemo, useRef } from "react"; import { NostrEvent } from "../../../types/nostr-event"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; import { DVMAvatarLink } from "./dvm-avatar"; -import { getEventAddressPointer, getEventUID } from "../../../helpers/nostr/events"; +import { getEventAddressPointer, getEventUID } from "../../../helpers/nostr/event"; import { DVMName } from "./dvm-name"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; diff --git a/src/views/dvm-feed/components/dvm-name.tsx b/src/views/dvm-feed/components/dvm-name.tsx index 19bfb08ef..ef53f11b7 100644 --- a/src/views/dvm-feed/components/dvm-name.tsx +++ b/src/views/dvm-feed/components/dvm-name.tsx @@ -2,8 +2,8 @@ import { Link, LinkProps, Text, TextProps } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; -import { getUserDisplayName } from "../../../helpers/user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../../helpers/nostr/user-metadata"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; import useDVMMetadata from "../../../hooks/use-dvm-metadata"; diff --git a/src/views/dvm-feed/components/feed-status.tsx b/src/views/dvm-feed/components/feed-status.tsx index 1f4bfed21..e615165b7 100644 --- a/src/views/dvm-feed/components/feed-status.tsx +++ b/src/views/dvm-feed/components/feed-status.tsx @@ -21,7 +21,7 @@ import { getJobStatusType, getResponseFromDVM, } from "../../../helpers/nostr/dvm"; -import { InlineInvoiceCard } from "../../../components/inline-invoice-card"; +import { InlineInvoiceCard } from "../../../components/lightning/inline-invoice-card"; import { DraftNostrEvent } from "../../../types/nostr-event"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { DVMAvatarLink } from "./dvm-avatar"; diff --git a/src/views/dvm-feed/index.tsx b/src/views/dvm-feed/index.tsx index b53071fbf..da3ff4412 100644 --- a/src/views/dvm-feed/index.tsx +++ b/src/views/dvm-feed/index.tsx @@ -7,7 +7,7 @@ import useTimelineLoader from "../../hooks/use-timeline-loader"; import { useReadRelays } from "../../hooks/use-client-relays"; import useSubject from "../../hooks/use-subject"; import RequireCurrentAccount from "../../providers/route/require-current-account"; -import { getEventCoordinate } from "../../helpers/nostr/events"; +import { getEventCoordinate } from "../../helpers/nostr/event"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; diff --git a/src/views/emoji-packs/browse.tsx b/src/views/emoji-packs/browse.tsx index fa1c25437..552191ea7 100644 --- a/src/views/emoji-packs/browse.tsx +++ b/src/views/emoji-packs/browse.tsx @@ -10,7 +10,7 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useSubject from "../../hooks/use-subject"; import EmojiPackCard from "./components/emoji-pack-card"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { EMOJI_PACK_KIND, getEmojisFromPack } from "../../helpers/nostr/emoji-packs"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/emoji-packs/components/emoji-pack-card.tsx b/src/views/emoji-packs/components/emoji-pack-card.tsx index c2c97f70f..5c8d8cc9c 100644 --- a/src/views/emoji-packs/components/emoji-pack-card.tsx +++ b/src/views/emoji-packs/components/emoji-pack-card.tsx @@ -13,13 +13,13 @@ import { Text, } from "@chakra-ui/react"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { getSharableEventAddress } from "../../../helpers/nip19"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import EmojiPackFavoriteButton from "./emoji-pack-favorite-button"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getEmojisFromPack, getPackName } from "../../../helpers/nostr/emoji-packs"; import EmojiPackMenu from "./emoji-pack-menu"; import NoteZapButton from "../../../components/note/note-zap-button"; diff --git a/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx b/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx index 3fc3bc409..c01518551 100644 --- a/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx +++ b/src/views/emoji-packs/components/emoji-pack-favorite-button.tsx @@ -4,7 +4,7 @@ import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { StarEmptyIcon, StarFullIcon } from "../../../components/icons"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import { USER_EMOJI_LIST_KIND } from "../../../helpers/nostr/emoji-packs"; import useFavoriteEmojiPacks from "../../../hooks/use-favorite-emoji-packs"; import { listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists"; diff --git a/src/views/emoji-packs/components/emoji-pack-menu.tsx b/src/views/emoji-packs/components/emoji-pack-menu.tsx index 8f298b956..307ccd928 100644 --- a/src/views/emoji-packs/components/emoji-pack-menu.tsx +++ b/src/views/emoji-packs/components/emoji-pack-menu.tsx @@ -1,5 +1,5 @@ import { NostrEvent } from "../../../types/nostr-event"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; @@ -11,12 +11,12 @@ export default function EmojiPackMenu({ }: { pack: NostrEvent } & Omit) { return ( <> - + - + ); } diff --git a/src/views/emoji-packs/emoji-pack.tsx b/src/views/emoji-packs/emoji-pack.tsx index 4bf503d7e..6da45f888 100644 --- a/src/views/emoji-packs/emoji-pack.tsx +++ b/src/views/emoji-packs/emoji-pack.tsx @@ -19,7 +19,7 @@ import { Text, } from "@chakra-ui/react"; -import UserLink from "../../components/user-link"; +import UserLink from "../../components/user/user-link"; import { ChevronLeftIcon } from "../../components/icons"; import useCurrentAccount from "../../hooks/use-current-account"; import { useDeleteEventContext } from "../../providers/route/delete-event-provider"; @@ -29,12 +29,12 @@ import EmojiPackFavoriteButton from "./components/emoji-pack-favorite-button"; import { EMOJI_PACK_KIND, getEmojisFromPack, getPackName } from "../../helpers/nostr/emoji-packs"; import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; import VerticalPageLayout from "../../components/vertical-page-layout"; -import UserAvatarLink from "../../components/user-avatar-link"; -import NoteZapButton from "../../components/note/note-zap-button"; -import QuoteRepostButton from "../../components/note/components/quote-repost-button"; +import UserAvatarLink from "../../components/user/user-avatar-link"; import Timestamp from "../../components/timestamp"; import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; import { usePublishEvent } from "../../providers/global/publish-provider"; +import NoteZapButton from "../../components/note/note-zap-button"; +import QuoteRepostButton from "../../components/note/quote-repost-button"; function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string }) => void }) { const { register, handleSubmit, watch, getValues, reset } = useForm({ @@ -73,7 +73,19 @@ function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string } function EmojiTag({ name, url, onRemove, scale }: { name: string; url: string; onRemove?: () => void; scale: number }) { return ( - + {`:${name}:`} {name} {onRemove && } diff --git a/src/views/emoji-packs/index.tsx b/src/views/emoji-packs/index.tsx index a2b91293b..07b973870 100644 --- a/src/views/emoji-packs/index.tsx +++ b/src/views/emoji-packs/index.tsx @@ -3,7 +3,7 @@ import { Link as RouterLink } from "react-router-dom"; import useCurrentAccount from "../../hooks/use-current-account"; import { ExternalLinkIcon } from "../../components/icons"; -import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID } from "../../helpers/nostr/event"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { EMOJI_PACK_KIND, getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs"; diff --git a/src/views/files/index.tsx b/src/views/files/index.tsx index 33d608d56..e7b63632b 100644 --- a/src/views/files/index.tsx +++ b/src/views/files/index.tsx @@ -7,12 +7,12 @@ import { NostrEvent } from "../../types/nostr-event"; import { FILE_KIND, IMAGE_TYPES, VIDEO_TYPES, getFileUrl, parseImageFile } from "../../helpers/nostr/files"; import { ErrorBoundary } from "../../components/error-boundary"; import useAppSettings from "../../hooks/use-app-settings"; -import { TrustProvider, useTrusted } from "../../providers/local/trust"; +import { TrustProvider, useTrustContext } from "../../providers/local/trust"; import BlurredImage from "../../components/blured-image"; import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; -import { UserAvatarLink } from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import { UserAvatarLink } from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import MimeTypePicker from "./mime-type-picker"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import VerticalPageLayout from "../../components/vertical-page-layout"; @@ -27,7 +27,7 @@ import { useReadRelays } from "../../hooks/use-client-relays"; function ImageFile({ event }: { event: NostrEvent }) { const parsed = parseImageFile(event); const settings = useAppSettings(); - const trust = useTrusted(); + const { trust } = useTrustContext(); const ref = useRef(null); useRegisterIntersectionEntity(ref, event.id); diff --git a/src/views/goals/browse.tsx b/src/views/goals/browse.tsx index 6e4b9b084..ec515ef6f 100644 --- a/src/views/goals/browse.tsx +++ b/src/views/goals/browse.tsx @@ -10,7 +10,7 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useSubject from "../../hooks/use-subject"; import GoalCard from "./components/goal-card"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { GOAL_KIND, getGoalClosedDate } from "../../helpers/nostr/goal"; import { NostrEvent } from "../../types/nostr-event"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/goals/components/goal-card.tsx b/src/views/goals/components/goal-card.tsx index 811f800da..a7e37e80b 100644 --- a/src/views/goals/components/goal-card.tsx +++ b/src/views/goals/components/goal-card.tsx @@ -2,12 +2,12 @@ import { memo, useRef } from "react"; import { Link as RouterLink } from "react-router-dom"; import { ButtonGroup, Card, CardBody, CardHeader, CardProps, Flex, Heading, Link, Text } from "@chakra-ui/react"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { getSharableEventAddress } from "../../../helpers/nip19"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getGoalClosedDate, getGoalName } from "../../../helpers/nostr/goal"; import GoalMenu from "./goal-menu"; import GoalProgress from "./goal-progress"; diff --git a/src/views/goals/components/goal-contents.tsx b/src/views/goals/components/goal-contents.tsx index 0c4496f94..15760f1bd 100644 --- a/src/views/goals/components/goal-contents.tsx +++ b/src/views/goals/components/goal-contents.tsx @@ -2,7 +2,7 @@ import { EmbedEventPointer } from "../../../components/embed-event"; import { getGoalEventPointers, getGoalLinks } from "../../../helpers/nostr/goal"; import { NostrEvent } from "../../../types/nostr-event"; import { encodeDecodeResult } from "../../../helpers/nip19"; -import OpenGraphCard from "../../../components/open-graph-card"; +import OpenGraphCard from "../../../components/open-graph/open-graph-card"; export default function GoalContents({ goal }: { goal: NostrEvent }) { const pointers = getGoalEventPointers(goal); diff --git a/src/views/goals/components/goal-menu.tsx b/src/views/goals/components/goal-menu.tsx index 57a972239..9d3eabc95 100644 --- a/src/views/goals/components/goal-menu.tsx +++ b/src/views/goals/components/goal-menu.tsx @@ -1,5 +1,5 @@ import { NostrEvent } from "../../../types/nostr-event"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item"; @@ -7,11 +7,11 @@ import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu export default function GoalMenu({ goal, ...props }: { goal: NostrEvent } & Omit) { return ( <> - + - + ); } diff --git a/src/views/goals/components/goal-progress.tsx b/src/views/goals/components/goal-progress.tsx index c5d70306d..d12977920 100644 --- a/src/views/goals/components/goal-progress.tsx +++ b/src/views/goals/components/goal-progress.tsx @@ -3,7 +3,7 @@ import { NostrEvent } from "../../../types/nostr-event"; import { getGoalAmount, getGoalRelays } from "../../../helpers/nostr/goal"; import { LightningIcon } from "../../../components/icons"; import useEventZaps from "../../../hooks/use-event-zaps"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { totalZaps } from "../../../helpers/nostr/zaps"; import { readablizeSats } from "../../../helpers/bolt11"; diff --git a/src/views/goals/components/goal-top-zappers.tsx b/src/views/goals/components/goal-top-zappers.tsx index e329fc09b..a5d5060d6 100644 --- a/src/views/goals/components/goal-top-zappers.tsx +++ b/src/views/goals/components/goal-top-zappers.tsx @@ -1,11 +1,11 @@ import { Box, Flex, FlexProps, Text } from "@chakra-ui/react"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getGoalRelays } from "../../../helpers/nostr/goal"; import useEventZaps from "../../../hooks/use-event-zaps"; import { NostrEvent } from "../../../types/nostr-event"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { readablizeSats } from "../../../helpers/bolt11"; import { LightningIcon } from "../../../components/icons"; diff --git a/src/views/goals/components/goal-zap-button.tsx b/src/views/goals/components/goal-zap-button.tsx index 0090a1156..e7f6f450c 100644 --- a/src/views/goals/components/goal-zap-button.tsx +++ b/src/views/goals/components/goal-zap-button.tsx @@ -2,7 +2,7 @@ import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react"; import { NostrEvent } from "../../../types/nostr-event"; import ZapModal from "../../../components/event-zap-modal"; import eventZapsService from "../../../services/event-zaps"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getGoalRelays } from "../../../helpers/nostr/goal"; import { useReadRelays } from "../../../hooks/use-client-relays"; diff --git a/src/views/goals/components/goal-zap-list.tsx b/src/views/goals/components/goal-zap-list.tsx index 9c1071ef4..1ab44de46 100644 --- a/src/views/goals/components/goal-zap-list.tsx +++ b/src/views/goals/components/goal-zap-list.tsx @@ -1,10 +1,10 @@ import { Box, Flex, Spacer, Text } from "@chakra-ui/react"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { getGoalRelays } from "../../../helpers/nostr/goal"; import useEventZaps from "../../../hooks/use-event-zaps"; import { NostrEvent } from "../../../types/nostr-event"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { readablizeSats } from "../../../helpers/bolt11"; import { LightningIcon } from "../../../components/icons"; import Timestamp from "../../../components/timestamp"; diff --git a/src/views/goals/goal-details.tsx b/src/views/goals/goal-details.tsx index c24dc0058..39248bc46 100644 --- a/src/views/goals/goal-details.tsx +++ b/src/views/goals/goal-details.tsx @@ -6,8 +6,8 @@ import GoalMenu from "./components/goal-menu"; import { getGoalAmount, getGoalName } from "../../helpers/nostr/goal"; import GoalProgress from "./components/goal-progress"; import useSingleEvent from "../../hooks/use-single-event"; -import UserAvatar from "../../components/user-avatar"; -import UserLink from "../../components/user-link"; +import UserAvatar from "../../components/user/user-avatar"; +import UserLink from "../../components/user/user-link"; import GoalContents from "./components/goal-contents"; import GoalZapList from "./components/goal-zap-list"; import { readablizeSats } from "../../helpers/bolt11"; diff --git a/src/views/goals/index.tsx b/src/views/goals/index.tsx index 20cbe97af..45f1071f7 100644 --- a/src/views/goals/index.tsx +++ b/src/views/goals/index.tsx @@ -3,7 +3,7 @@ import { Navigate, Link as RouterLink } from "react-router-dom"; import useCurrentAccount from "../../hooks/use-current-account"; import { ExternalLinkIcon } from "../../components/icons"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; diff --git a/src/views/hashtag/index.tsx b/src/views/hashtag/index.tsx index 945cae54b..231731c74 100644 --- a/src/views/hashtag/index.tsx +++ b/src/views/hashtag/index.tsx @@ -15,7 +15,7 @@ import { CloseIcon } from "@chakra-ui/icons"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useAppTitle } from "../../hooks/use-app-title"; import useTimelineLoader from "../../hooks/use-timeline-loader"; -import { isReply, isRepost } from "../../helpers/nostr/events"; +import { isReply, isRepost } from "../../helpers/nostr/event"; import { CheckIcon, EditIcon } from "../../components/icons"; import { NostrEvent } from "../../types/nostr-event"; import useRelaysChanged from "../../hooks/use-relays-changed"; diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx index 4a8114132..a92a9733f 100644 --- a/src/views/home/index.tsx +++ b/src/views/home/index.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react"; import { Flex, Spacer, useDisclosure } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; -import { isReply, isRepost } from "../../helpers/nostr/events"; +import { isReply, isRepost } from "../../helpers/nostr/event"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { NostrEvent } from "../../types/nostr-event"; import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page"; diff --git a/src/views/launchpad/components/dms-card.tsx b/src/views/launchpad/components/dms-card.tsx index f7e0a663b..195492eee 100644 --- a/src/views/launchpad/components/dms-card.tsx +++ b/src/views/launchpad/components/dms-card.tsx @@ -14,10 +14,10 @@ import { sortConversationsByLastReceived, } from "../../../helpers/nostr/dms"; import { NostrEvent } from "../../../types/nostr-event"; -import UserAvatar from "../../../components/user-avatar"; +import UserAvatar from "../../../components/user/user-avatar"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import UserName from "../../../components/user-name"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import UserName from "../../../components/user/user-name"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import { nip19 } from "nostr-tools"; import { useDecryptionContainer, useDecryptionContext } from "../../../providers/global/dycryption-provider"; import Timestamp from "../../../components/timestamp"; diff --git a/src/views/launchpad/components/feeds-card.tsx b/src/views/launchpad/components/feeds-card.tsx index 7adb0ee91..ee7333c64 100644 --- a/src/views/launchpad/components/feeds-card.tsx +++ b/src/views/launchpad/components/feeds-card.tsx @@ -16,10 +16,10 @@ import { Link as RouterLink } from "react-router-dom"; import useUserLists from "../../../hooks/use-user-lists"; import { NostrEvent } from "../../../types/nostr-event"; import { PEOPLE_LIST_KIND, getListName, getPubkeysFromList } from "../../../helpers/nostr/lists"; -import UserAvatar from "../../../components/user-avatar"; +import UserAvatar from "../../../components/user/user-avatar"; import useCurrentAccount from "../../../hooks/use-current-account"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events"; +import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/event"; import Plus from "../../../components/icons/plus"; import useUserContactList from "../../../hooks/use-user-contact-list"; import useRecentIds from "../../../hooks/use-recent-ids"; diff --git a/src/views/launchpad/components/search-form.tsx b/src/views/launchpad/components/search-form.tsx index 71e42524e..8198cc5fd 100644 --- a/src/views/launchpad/components/search-form.tsx +++ b/src/views/launchpad/components/search-form.tsx @@ -17,8 +17,8 @@ import { nip19 } from "nostr-tools"; import { Link as RouterLink } from "react-router-dom"; import { useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider"; -import UserAvatar from "../../../components/user-avatar"; -import UserName from "../../../components/user-name"; +import UserAvatar from "../../../components/user/user-avatar"; +import UserName from "../../../components/user/user-name"; import KeyboardShortcut from "../../../components/keyboard-shortcut"; function UserOption({ pubkey }: { pubkey: string }) { diff --git a/src/views/lists/browse.tsx b/src/views/lists/browse.tsx index a7b8a1df2..6b42bc054 100644 --- a/src/views/lists/browse.tsx +++ b/src/views/lists/browse.tsx @@ -18,7 +18,7 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useSubject from "../../hooks/use-subject"; import ListCard from "./components/list-card"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import VerticalPageLayout from "../../components/vertical-page-layout"; function BrowseListPage() { diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx index 58b153626..477219ccc 100644 --- a/src/views/lists/components/list-card.tsx +++ b/src/views/lists/components/list-card.tsx @@ -15,8 +15,8 @@ import { } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import { getEventPointersFromList, getListDescription, @@ -29,10 +29,9 @@ import { import { getSharableEventAddress } from "../../../helpers/nip19"; import { NostrEvent } from "../../../types/nostr-event"; import useReplaceableEvent from "../../../hooks/use-replaceable-event"; -import { createCoordinate } from "../../../services/replaceable-event-requester"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import ListFavoriteButton from "./list-favorite-button"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import ListMenu from "./list-menu"; import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities"; import { CommunityIcon, NotesIcon } from "../../../components/icons"; @@ -42,6 +41,7 @@ import NoteZapButton from "../../../components/note/note-zap-button"; import Link01 from "../../../components/icons/link-01"; import File02 from "../../../components/icons/file-02"; import SimpleLikeButton from "../../../components/event-reactions/simple-like-button"; +import { createCoordinate } from "../../../classes/batch-kind-loader"; export function ListCardContent({ list, ...props }: Omit & { list: NostrEvent }) { const people = getPubkeysFromList(list); diff --git a/src/views/lists/components/list-favorite-button.tsx b/src/views/lists/components/list-favorite-button.tsx index 5732c768b..5deeedcb2 100644 --- a/src/views/lists/components/list-favorite-button.tsx +++ b/src/views/lists/components/list-favorite-button.tsx @@ -4,7 +4,7 @@ import dayjs from "dayjs"; import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event"; import { StarEmptyIcon, StarFullIcon } from "../../../components/icons"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import useFavoriteLists, { FAVORITE_LISTS_IDENTIFIER } from "../../../hooks/use-favorite-lists"; import { NOTE_LIST_KIND, diff --git a/src/views/lists/components/list-feed-button.tsx b/src/views/lists/components/list-feed-button.tsx index 39bc15401..db7201375 100644 --- a/src/views/lists/components/list-feed-button.tsx +++ b/src/views/lists/components/list-feed-button.tsx @@ -3,7 +3,7 @@ import { Link as RouterLink } from "react-router-dom"; import { kinds } from "nostr-tools"; import { NostrEvent } from "../../../types/nostr-event"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import { PEOPLE_LIST_KIND } from "../../../helpers/nostr/lists"; export default function ListFeedButton({ list, ...props }: { list: NostrEvent } & Omit) { diff --git a/src/views/lists/components/list-menu.tsx b/src/views/lists/components/list-menu.tsx index 8074bc40f..386946616 100644 --- a/src/views/lists/components/list-menu.tsx +++ b/src/views/lists/components/list-menu.tsx @@ -1,7 +1,7 @@ import { Image, MenuItem } from "@chakra-ui/react"; import { NostrEvent, isPTag } from "../../../types/nostr-event"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { getSharableEventAddress } from "../../../helpers/nip19"; import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; @@ -17,20 +17,20 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit return ( <> - + {!isSpecial && } {hasPeople && ( } + icon={} onClick={() => window.open(`https://www.makeprisms.com/create/${naddr}`, "_blank")} > Create $prism )} - + ); } diff --git a/src/views/lists/components/note-card.tsx b/src/views/lists/components/note-card.tsx index 38910b899..df7850b12 100644 --- a/src/views/lists/components/note-card.tsx +++ b/src/views/lists/components/note-card.tsx @@ -1,15 +1,15 @@ import { Text } from "@chakra-ui/react"; -import { Note } from "../../../components/note"; -import { NoteLink } from "../../../components/note-link"; +import { NoteLink } from "../../../components/note/note-link"; import { useReadRelays } from "../../../hooks/use-client-relays"; import useSingleEvent from "../../../hooks/use-single-event"; +import TimelineNote from "../../../components/note/timeline-note"; export default function NoteCard({ id, relay }: { id: string; relay?: string }) { const readRelays = useReadRelays(relay ? [relay] : []); const event = useSingleEvent(id, readRelays); return event ? ( - + ) : ( Loading diff --git a/src/views/lists/components/user-card.tsx b/src/views/lists/components/user-card.tsx index 77588e0c1..6be80fe69 100644 --- a/src/views/lists/components/user-card.tsx +++ b/src/views/lists/components/user-card.tsx @@ -2,15 +2,15 @@ import { Button, Card, CardBody, CardProps, Flex, Heading, Link } from "@chakra- import { Link as RouterLink } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; -import { getUserDisplayName } from "../../../helpers/user-metadata"; -import UserAvatar from "../../../components/user-avatar"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import useUserMetadata from "../../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../../helpers/nostr/user-metadata"; +import UserAvatar from "../../../components/user/user-avatar"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import { NostrEvent } from "../../../types/nostr-event"; import useAsyncErrorHandler from "../../../hooks/use-async-error-handler"; import { listRemovePerson } from "../../../helpers/nostr/lists"; import useCurrentAccount from "../../../hooks/use-current-account"; -import { UserFollowButton } from "../../../components/user-follow-button"; +import { UserFollowButton } from "../../../components/user/user-follow-button"; import { usePublishEvent } from "../../../providers/global/publish-provider"; export type UserCardProps = { pubkey: string; relay?: string; list: NostrEvent } & Omit; diff --git a/src/views/lists/index.tsx b/src/views/lists/index.tsx index adf48f804..dca835008 100644 --- a/src/views/lists/index.tsx +++ b/src/views/lists/index.tsx @@ -5,7 +5,7 @@ import { kinds } from "nostr-tools"; import useCurrentAccount from "../../hooks/use-current-account"; import { ExternalLinkIcon, PlusCircleIcon } from "../../components/icons"; import ListCard from "./components/list-card"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import useUserLists from "../../hooks/use-user-lists"; import NewListModal from "./components/new-list-modal"; import { getSharableEventAddress } from "../../helpers/nip19"; diff --git a/src/views/lists/list/index.tsx b/src/views/lists/list/index.tsx index d69d88c2f..c18e111cb 100644 --- a/src/views/lists/list/index.tsx +++ b/src/views/lists/list/index.tsx @@ -3,7 +3,7 @@ import { kinds, nip19 } from "nostr-tools"; import type { DecodeResult } from "nostr-tools/lib/types/nip19"; import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; import { ChevronLeftIcon } from "../../../components/icons"; import useCurrentAccount from "../../../hooks/use-current-account"; import { useDeleteEventContext } from "../../../providers/route/delete-event-provider"; @@ -18,7 +18,7 @@ import { } from "../../../helpers/nostr/lists"; import useReplaceableEvent from "../../../hooks/use-replaceable-event"; import UserCard from "../components/user-card"; -import OpenGraphCard from "../../../components/open-graph-card"; +import OpenGraphCard from "../../../components/open-graph/open-graph-card"; import { TrustProvider } from "../../../providers/local/trust"; import ListMenu from "../components/list-menu"; import ListFavoriteButton from "../components/list-favorite-button"; @@ -28,7 +28,7 @@ import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities"; import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event"; import { encodeDecodeResult } from "../../../helpers/nip19"; import useSingleEvent from "../../../hooks/use-single-event"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; import useParamsAddressPointer from "../../../hooks/use-params-address-pointer"; import { NostrEvent } from "../../../types/nostr-event"; diff --git a/src/views/map/timeline.tsx b/src/views/map/timeline.tsx index f1c9406ff..c3885d25f 100644 --- a/src/views/map/timeline.tsx +++ b/src/views/map/timeline.tsx @@ -4,15 +4,15 @@ import { kinds } from "nostr-tools"; import { ErrorBoundary } from "../../components/error-boundary"; import useSubject from "../../hooks/use-subject"; import StreamNote from "../../components/timeline-page/generic-note-timeline/stream-note"; -import { Note } from "../../components/note"; import { STREAM_KIND } from "../../helpers/nostr/stream"; import TimelineLoader from "../../classes/timeline-loader"; import { NostrEvent } from "../../types/nostr-event"; +import TimelineNote from "../../components/note/timeline-note"; const RenderEvent = React.memo(({ event, focused }: { event: NostrEvent; focused?: boolean }) => { switch (event.kind) { case kinds.ShortTextNote: - return ; + return ; case STREAM_KIND: return ; default: diff --git a/src/views/notifications/index.tsx b/src/views/notifications/index.tsx index eec8703f9..e592ad852 100644 --- a/src/views/notifications/index.tsx +++ b/src/views/notifications/index.tsx @@ -11,7 +11,7 @@ import IntersectionObserverProvider, { import useSubject from "../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { useNotificationTimeline } from "../../providers/global/notification-timeline"; -import { getEventUID, isReply, isRepost } from "../../helpers/nostr/events"; +import { getEventUID, isReply, isRepost } from "../../helpers/nostr/event"; import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/notifications/notification-item.tsx b/src/views/notifications/notification-item.tsx index 62ece776d..db446d954 100644 --- a/src/views/notifications/notification-item.tsx +++ b/src/views/notifications/notification-item.tsx @@ -7,13 +7,13 @@ import { NostrEvent, isATag, isETag } from "../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../providers/local/intersection-observer"; import { parseZapEvent } from "../../helpers/nostr/zaps"; import { readablizeSats } from "../../helpers/bolt11"; -import { getEventUID, getThreadReferences, isMentionedInContent, parseCoordinate } from "../../helpers/nostr/events"; +import { getEventUID, getThreadReferences, isMentionedInContent, parseCoordinate } from "../../helpers/nostr/event"; import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event"; import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown"; import { ErrorBoundary } from "../../components/error-boundary"; import { TrustProvider } from "../../providers/local/trust"; import Heart from "../../components/icons/heart"; -import UserAvatarLink from "../../components/user-avatar-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; import { AtIcon, ChevronDownIcon, ChevronUpIcon, LightningIcon, ReplyIcon, RepostIcon } from "../../components/icons"; import useSingleEvent from "../../hooks/use-single-event"; import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents"; diff --git a/src/views/notifications/threads.tsx b/src/views/notifications/threads.tsx index b6a16bf64..6efe8e0e8 100644 --- a/src/views/notifications/threads.tsx +++ b/src/views/notifications/threads.tsx @@ -15,9 +15,9 @@ import { NostrEvent } from "../../types/nostr-event"; import NotificationIconEntry from "./components/notification-icon-entry"; import { ChevronLeftIcon, ReplyIcon } from "../../components/icons"; import { AvatarGroup, Box, Button, ButtonGroup, Flex, LinkBox, Text, useDisclosure } from "@chakra-ui/react"; -import UserAvatarLink from "../../components/user-avatar-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; import useSingleEvent from "../../hooks/use-single-event"; -import UserLink from "../../components/user-link"; +import UserLink from "../../components/user/user-link"; import { CompactNoteContent } from "../../components/compact-note-content"; import Timestamp from "../../components/timestamp"; import HoverLinkOverlay from "../../components/hover-link-overlay"; @@ -26,7 +26,7 @@ import PeopleListSelection from "../../components/people-list-selection/people-l import IntersectionObserverProvider, { useRegisterIntersectionEntity, } from "../../providers/local/intersection-observer"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { useNavigateInDrawer } from "../../providers/drawer-sub-view-provider"; const THREAD_KINDS = [kinds.ShortTextNote, TORRENT_COMMENT_KIND]; diff --git a/src/views/other-stuff/apps.ts b/src/views/other-stuff/apps.ts index 134de469d..19758ac28 100644 --- a/src/views/other-stuff/apps.ts +++ b/src/views/other-stuff/apps.ts @@ -10,6 +10,7 @@ import { LiveStreamIcon, MapIcon, MuteIcon, + SearchIcon, TorrentIcon, TrackIcon, } from "../../components/icons"; @@ -18,6 +19,7 @@ import ShieldOff from "../../components/icons/shield-off"; import Users01 from "../../components/icons/users-01"; import Film02 from "../../components/icons/film-02"; import MessageQuestionSquare from "../../components/icons/message-question-square"; +import UploadCloud01 from "../../components/icons/upload-cloud-01"; export const internalApps: App[] = [ { @@ -88,6 +90,20 @@ export const internalTools: App[] = [ id: "unknown", to: "/tools/unknown", }, + { + title: "Event Console", + description: "Find events based on nostr filters", + icon: SearchIcon, + id: "console", + to: "/tools/console", + }, + { + title: "Event Publisher", + description: "Write and publish events", + icon: UploadCloud01, + id: "publisher", + to: "/tools/publisher ", + }, { title: "WoT Test", description: "Just a test for now", icon: Users01, id: "wot-test", to: "/tools/wot-test" }, ]; diff --git a/src/views/profile/edit.tsx b/src/views/profile/edit.tsx index 4f53cbc96..a4413ea33 100644 --- a/src/views/profile/edit.tsx +++ b/src/views/profile/edit.tsx @@ -15,10 +15,10 @@ import { useForm } from "react-hook-form"; import { ExternalLinkIcon } from "../../components/icons"; import { isLNURL } from "../../helpers/lnurl"; -import { Kind0ParsedContent } from "../../helpers/user-metadata"; +import { Kind0ParsedContent } from "../../helpers/nostr/user-metadata"; import { useReadRelays } from "../../hooks/use-client-relays"; import useCurrentAccount from "../../hooks/use-current-account"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; import dnsIdentityService from "../../services/dns-identity"; import { DraftNostrEvent } from "../../types/nostr-event"; import lnurlMetadataService from "../../services/lnurl-metadata"; @@ -205,27 +205,27 @@ export const ProfileEditView = () => { ); const handleSubmit = async (data: FormData) => { - const metadata: Kind0ParsedContent = { + const newMetadata: Kind0ParsedContent = { name: data.username, picture: data.picture, }; - if (data.displayName) metadata.displayName = metadata.display_name = data.displayName; - if (data.about) metadata.about = data.about; - if (data.website) metadata.website = data.website; - if (data.nip05) metadata.nip05 = data.nip05; + if (data.displayName) newMetadata.displayName = newMetadata.display_name = data.displayName; + if (data.about) newMetadata.about = data.about; + if (data.website) newMetadata.website = data.website; + if (data.nip05) newMetadata.nip05 = data.nip05; if (data.lightningAddress) { if (isLNURL(data.lightningAddress)) { - metadata.lud06 = data.lightningAddress; + newMetadata.lud06 = data.lightningAddress; } else if (isLightningAddress(data.lightningAddress)) { - metadata.lud16 = data.lightningAddress; + newMetadata.lud16 = data.lightningAddress; } } const draft: DraftNostrEvent = { created_at: dayjs().unix(), kind: 0, - content: JSON.stringify(metadata), + content: JSON.stringify({ ...metadata, ...newMetadata }), tags: [], }; diff --git a/src/views/relays/app/index.tsx b/src/views/relays/app/index.tsx index 78e8dbd5b..4c355bca4 100644 --- a/src/views/relays/app/index.tsx +++ b/src/views/relays/app/index.tsx @@ -5,7 +5,7 @@ import useSubject from "../../../hooks/use-subject"; import { offlineMode } from "../../../services/offline-mode"; import WifiOff from "../../../components/icons/wifi-off"; import Wifi from "../../../components/icons/wifi"; -import BackButton from "../../../components/back-button"; +import BackButton from "../../../components/router/back-button"; import AddRelayForm from "./add-relay-form"; import clientRelaysService from "../../../services/client-relays"; import { RelayMode } from "../../../classes/relay"; diff --git a/src/views/relays/app/select-relay-set.tsx b/src/views/relays/app/select-relay-set.tsx index c3eb8ebb5..faedc76b6 100644 --- a/src/views/relays/app/select-relay-set.tsx +++ b/src/views/relays/app/select-relay-set.tsx @@ -3,7 +3,7 @@ import { NostrEvent } from "nostr-tools"; import useCurrentAccount from "../../../hooks/use-current-account"; import useUserRelaySets from "../../../hooks/use-user-relay-sets"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import { getListName } from "../../../helpers/nostr/lists"; export default function SelectRelaySet({ diff --git a/src/views/relays/cache/index.tsx b/src/views/relays/cache/index.tsx index ea075b9e6..b21cc7265 100644 --- a/src/views/relays/cache/index.tsx +++ b/src/views/relays/cache/index.tsx @@ -1,5 +1,5 @@ import { Button, Card, CardBody, CardHeader, Flex, Heading, Link, Text } from "@chakra-ui/react"; -import BackButton from "../../../components/back-button"; +import BackButton from "../../../components/router/back-button"; import { useAsync } from "react-use"; import { NOSTR_RELAY_TRAY_URL, checkNostrRelayTray, localRelay } from "../../../services/local-relay"; import { CacheRelay } from "nostr-idb"; diff --git a/src/views/relays/components/add-custom-modal.tsx b/src/views/relays/components/add-custom-modal.tsx index 52197aca6..6a33bc3db 100644 --- a/src/views/relays/components/add-custom-modal.tsx +++ b/src/views/relays/components/add-custom-modal.tsx @@ -21,10 +21,10 @@ import { } from "@chakra-ui/react"; import { useState } from "react"; import { useRelayInfo } from "../../../hooks/use-relay-info"; -import UserAvatar from "../../../components/user-avatar"; -import UserLink from "../../../components/user-link"; +import UserAvatar from "../../../components/user/user-avatar"; +import UserLink from "../../../components/user/user-link"; import { useDebounce } from "react-use"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import { CodeIcon } from "../../../components/icons"; import { Metadata } from "./relay-card"; import { safeRelayUrl } from "../../../helpers/relay"; diff --git a/src/views/relays/components/relay-card.tsx b/src/views/relays/components/relay-card.tsx index 0bcba68b3..6480bc9fe 100644 --- a/src/views/relays/components/relay-card.tsx +++ b/src/views/relays/components/relay-card.tsx @@ -25,9 +25,9 @@ import { Link as RouterLink } from "react-router-dom"; import { useRelayInfo } from "../../../hooks/use-relay-info"; import { RelayFavicon } from "../../../components/relay-favicon"; import { CodeIcon } from "../../../components/icons"; -import UserLink from "../../../components/user-link"; -import UserAvatar from "../../../components/user-avatar"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import UserLink from "../../../components/user/user-link"; +import UserAvatar from "../../../components/user/user-avatar"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import RawJson from "../../../components/debug-modal/raw-json"; import { RelayShareButton } from "./relay-share-button"; import useRelayStats from "../../../hooks/use-relay-stats"; diff --git a/src/views/relays/components/relay-review-note.tsx b/src/views/relays/components/relay-review-note.tsx index 0d5d65d9a..2b6ec429f 100644 --- a/src/views/relays/components/relay-review-note.tsx +++ b/src/views/relays/components/relay-review-note.tsx @@ -2,17 +2,17 @@ import { useRef } from "react"; import { Card, CardBody, CardHeader, Link } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import StarRating from "../../../components/star-rating"; import { safeJson } from "../../../helpers/parse"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { NoteContents } from "../../../components/note/text-note-contents"; import { Metadata } from "./relay-card"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import Timestamp from "../../../components/timestamp"; +import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents"; export default function RelayReviewNote({ event, hideUrl }: { event: NostrEvent; hideUrl?: boolean }) { const ratingJson = event.tags.find((t) => t[0] === "l" && t[3])?.[3]; @@ -40,7 +40,7 @@ export default function RelayReviewNote({ event, hideUrl }: { event: NostrEvent; )} {rating && } - + ); diff --git a/src/views/relays/contact-list/index.tsx b/src/views/relays/contact-list/index.tsx index a5430bd1c..9a52c749f 100644 --- a/src/views/relays/contact-list/index.tsx +++ b/src/views/relays/contact-list/index.tsx @@ -1,5 +1,5 @@ import { Button, Code, Flex, Heading, Link, Spinner, Text } from "@chakra-ui/react"; -import BackButton from "../../../components/back-button"; +import BackButton from "../../../components/router/back-button"; import useCurrentAccount from "../../../hooks/use-current-account"; import { Link as RouterLink } from "react-router-dom"; @@ -9,7 +9,7 @@ import { CheckIcon, InboxIcon, OutboxIcon } from "../../../components/icons"; import { useCallback, useState } from "react"; import useCacheForm from "../../../hooks/use-cache-form"; import useUserContactList from "../../../hooks/use-user-contact-list"; -import { cloneEvent } from "../../../helpers/nostr/events"; +import { cloneEvent } from "../../../helpers/nostr/event"; import { EventTemplate } from "nostr-tools"; import dayjs from "dayjs"; import { usePublishEvent } from "../../../providers/global/publish-provider"; diff --git a/src/views/relays/index.tsx b/src/views/relays/index.tsx index 5451e0df5..aa9e168ee 100644 --- a/src/views/relays/index.tsx +++ b/src/views/relays/index.tsx @@ -5,7 +5,7 @@ import VerticalPageLayout from "../../components/vertical-page-layout"; import useCurrentAccount from "../../hooks/use-current-account"; import useUserRelaySets from "../../hooks/use-user-relay-sets"; import { getListName } from "../../helpers/nostr/lists"; -import { getEventCoordinate } from "../../helpers/nostr/events"; +import { getEventCoordinate } from "../../helpers/nostr/event"; import { useBreakpointValue } from "../../providers/global/breakpoint-provider"; import Database01 from "../../components/icons/database-01"; import { AtIcon, RelayIcon } from "../../components/icons"; @@ -103,7 +103,7 @@ export default function RelaysView() { else return nav; } else return ( - + {nav} diff --git a/src/views/relays/mailboxes/index.tsx b/src/views/relays/mailboxes/index.tsx index e788f527a..2f29047e7 100644 --- a/src/views/relays/mailboxes/index.tsx +++ b/src/views/relays/mailboxes/index.tsx @@ -29,7 +29,7 @@ import { useForm } from "react-hook-form"; import { safeRelayUrl } from "../../../helpers/relay"; import { usePublishEvent } from "../../../providers/global/publish-provider"; import { COMMON_CONTACT_RELAY } from "../../../const"; -import BackButton from "../../../components/back-button"; +import BackButton from "../../../components/router/back-button"; import AddRelayForm from "../app/add-relay-form"; function RelayLine({ relay, mode, list }: { relay: string; mode: RelayMode; list?: NostrEvent }) { diff --git a/src/views/relays/nip05/index.tsx b/src/views/relays/nip05/index.tsx index aee931a5f..5e270c1ba 100644 --- a/src/views/relays/nip05/index.tsx +++ b/src/views/relays/nip05/index.tsx @@ -1,5 +1,5 @@ import { Code, Flex, Heading, Link, Text } from "@chakra-ui/react"; -import BackButton from "../../../components/back-button"; +import BackButton from "../../../components/router/back-button"; import useCurrentAccount from "../../../hooks/use-current-account"; import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity"; import { Link as RouterLink } from "react-router-dom"; diff --git a/src/views/relays/popular.tsx b/src/views/relays/popular.tsx index 92b97101a..8ee116ec5 100644 --- a/src/views/relays/popular.tsx +++ b/src/views/relays/popular.tsx @@ -25,7 +25,7 @@ import userMailboxesService from "../../services/user-mailboxes"; import { NostrEvent } from "../../types/nostr-event"; import { RelayFavicon } from "../../components/relay-favicon"; import { ChevronLeftIcon } from "../../components/icons"; -import UserAvatar from "../../components/user-avatar"; +import UserAvatar from "../../components/user/user-avatar"; import { RelayMetadata, RelayPaidTag } from "./components/relay-card"; function usePopularContactsRelays(list?: NostrEvent) { diff --git a/src/views/relays/relay/index.tsx b/src/views/relays/relay/index.tsx index 49985370a..8610d26b9 100644 --- a/src/views/relays/relay/index.tsx +++ b/src/views/relays/relay/index.tsx @@ -1,4 +1,4 @@ -import { lazy } from "react"; +import { lazy, useMemo } from "react"; import { useParams } from "react-router-dom"; import { Button, @@ -34,8 +34,14 @@ function RelayPage({ relay }: { relay: string }) { const { info } = useRelayInfo(relay); const showReviewForm = useDisclosure(); + const uiURL = useMemo(() => { + const url = new URL(relay); + url.protocol = url.protocol === "wss:" ? "https:" : "http:"; + return url.toString(); + }, [relay]); + return ( - + @@ -64,12 +70,13 @@ function RelayPage({ relay }: { relay: string }) { Reviews + UI Notes Users Details - + @@ -82,6 +89,9 @@ function RelayPage({ relay }: { relay: string }) { {showReviewForm.isOpen && } + + + @@ -93,7 +103,7 @@ function RelayPage({ relay }: { relay: string }) { - + ); } diff --git a/src/views/relays/relay/relay-details.tsx b/src/views/relays/relay/relay-details.tsx index c96c7afa0..da48b297f 100644 --- a/src/views/relays/relay/relay-details.tsx +++ b/src/views/relays/relay/relay-details.tsx @@ -38,8 +38,8 @@ import { groupByTime } from "../../../helpers/notification"; import { useCallback, useEffect, useMemo, useState } from "react"; import EventStore from "../../../classes/event-store"; import NostrRequest from "../../../classes/nostr-request"; -import { sortByDate } from "../../../helpers/nostr/events"; -import { NostrQuery } from "../../../types/nostr-query"; +import { sortByDate } from "../../../helpers/nostr/event"; +import { NostrQuery } from "../../../types/nostr-relay"; ChartJS.register( ArcElement, @@ -155,9 +155,11 @@ export default function RelayDetailsTab({ relay }: { relay: string }) { const loadMore = useCallback(() => { setLoading(true); const request = new NostrRequest([relay]); - request.onEvent.subscribe(store.addEvent, store); const throttle = _throttle(() => update({}), 100); - request.onEvent.subscribe(() => throttle()); + request.onEvent.subscribe((e) => { + store.addEvent(e); + throttle(); + }); request.onComplete.then(() => setLoading(false)); const query: NostrQuery = { limit: 500 }; diff --git a/src/views/relays/relay/relay-notes.tsx b/src/views/relays/relay/relay-notes.tsx index 7c149ce13..d5d30119a 100644 --- a/src/views/relays/relay/relay-notes.tsx +++ b/src/views/relays/relay/relay-notes.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { Flex, Spacer, useDisclosure } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; -import { isReply, isRepost } from "../../../helpers/nostr/events"; +import { isReply, isRepost } from "../../../helpers/nostr/event"; import { useAppTitle } from "../../../hooks/use-app-title"; import useTimelineLoader from "../../../hooks/use-timeline-loader"; import { NostrEvent } from "../../../types/nostr-event"; diff --git a/src/views/relays/relay/relay-users.tsx b/src/views/relays/relay/relay-users.tsx index 7aacb97f9..2899b9294 100644 --- a/src/views/relays/relay/relay-users.tsx +++ b/src/views/relays/relay/relay-users.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import { Card, Flex, LinkBox, SimpleGrid } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { useAppTitle } from "../../../hooks/use-app-title"; import useTimelineLoader from "../../../hooks/use-timeline-loader"; import PeopleListSelection from "../../../components/people-list-selection/people-list-selection"; @@ -13,10 +13,10 @@ import IntersectionObserverProvider, { import useSubject from "../../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; import { getRelaysFromList } from "../../../helpers/nostr/lists"; import { getRelayVariations } from "../../../helpers/relay"; import { NostrEvent } from "../../../types/nostr-event"; diff --git a/src/views/search/community-results.tsx b/src/views/search/community-results.tsx index 71c2b213a..09fbedeb9 100644 --- a/src/views/search/community-results.tsx +++ b/src/views/search/community-results.tsx @@ -6,7 +6,7 @@ import IntersectionObserverProvider, { useRegisterIntersectionEntity, } from "../../providers/local/intersection-observer"; import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { NostrEvent } from "../../types/nostr-event"; import CommunityCard from "../communities/components/community-card"; import useSubject from "../../hooks/use-subject"; diff --git a/src/views/search/index.tsx b/src/views/search/index.tsx index e10811c4a..92f426a97 100644 --- a/src/views/search/index.tsx +++ b/src/views/search/index.tsx @@ -17,7 +17,7 @@ import PeopleListProvider from "../../providers/local/people-list-provider"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; import useRouteSearchValue from "../../hooks/use-route-search-value"; import { useBreakpointValue } from "../../providers/global/breakpoint-provider"; -import QRCodeScannerButton from "../../components/qr-code-scanner-button"; +import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button"; import { AdditionalRelayProvider } from "../../providers/local/additional-relay-context"; export function SearchPage() { diff --git a/src/views/search/profile-results.tsx b/src/views/search/profile-results.tsx index 73c18a7a6..b0a8e1909 100644 --- a/src/views/search/profile-results.tsx +++ b/src/views/search/profile-results.tsx @@ -4,13 +4,13 @@ import { useAsync } from "react-use"; import { kinds } from "nostr-tools"; import { NostrEvent } from "../../types/nostr-event"; -import { parseKind0Event } from "../../helpers/user-metadata"; +import { parseMetadataContent } from "../../helpers/nostr/user-metadata"; import { readablizeSats } from "../../helpers/bolt11"; import { EmbedableContent, embedUrls } from "../../helpers/embeds"; -import UserAvatar from "../../components/user-avatar"; -import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; +import UserAvatar from "../../components/user/user-avatar"; +import { UserDnsIdentityIcon } from "../../components/user/user-dns-identity-icon"; import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types"; -import UserLink from "../../components/user-link"; +import UserLink from "../../components/user/user-link"; import trustedUserStatsService, { NostrBandUserStats } from "../../services/trusted-user-stats"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; @@ -21,7 +21,7 @@ import { usePeopleListContext } from "../../providers/local/people-list-provider import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; function ProfileResult({ profile }: { profile: NostrEvent }) { - const metadata = parseKind0Event(profile); + const metadata = parseMetadataContent(profile); const aboutContent = useMemo(() => { if (!metadata.about) return null; diff --git a/src/views/settings/database-settings.tsx b/src/views/settings/database-settings.tsx index 56d0baae8..20c52e588 100644 --- a/src/views/settings/database-settings.tsx +++ b/src/views/settings/database-settings.tsx @@ -10,6 +10,7 @@ import { ButtonGroup, Text, Input, + Flex, } from "@chakra-ui/react"; import { addEvents, countEvents, countEventsByKind, getEventUID, updateUsed } from "nostr-idb"; import stringify from "json-stringify-deterministic"; @@ -98,7 +99,7 @@ export default function DatabaseSettings() { setExporting(true); const rows = await localDatabase.getAll("events"); const lines = rows.map((row) => stringify(row.event)); - const file = new File(lines, "noStrudel-export.jsonl", { + const file = new File([lines.join("\n")], "noStrudel-export.jsonl", { type: "application/jsonl", }); const url = URL.createObjectURL(file); @@ -119,7 +120,7 @@ export default function DatabaseSettings() { - + @@ -130,7 +131,7 @@ export default function DatabaseSettings() { - +
); diff --git a/src/views/settings/index.tsx b/src/views/settings/index.tsx index 803d2e0a6..833ff4c29 100644 --- a/src/views/settings/index.tsx +++ b/src/views/settings/index.tsx @@ -1,6 +1,5 @@ import { Button, Flex, Accordion, Link, useToast } from "@chakra-ui/react"; -import { Link as RouterLink } from "react-router-dom"; -import { GithubIcon, ToolsIcon } from "../../components/icons"; +import { GithubIcon } from "../../components/icons"; import LightningSettings from "./lightning-settings"; import DatabaseSettings from "./database-settings"; import DisplaySettings from "./display-settings"; diff --git a/src/views/settings/performance-settings.tsx b/src/views/settings/performance-settings.tsx index 4b190210a..a7c6214ba 100644 --- a/src/views/settings/performance-settings.tsx +++ b/src/views/settings/performance-settings.tsx @@ -13,17 +13,6 @@ import { Input, Link, FormErrorMessage, - Code, - Button, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalCloseButton, - ModalBody, - useDisclosure, - Text, - Heading, } from "@chakra-ui/react"; import { safeUrl } from "../../helpers/parse"; import { AppSettings } from "../../services/settings/migrations"; @@ -31,7 +20,6 @@ import { PerformanceIcon } from "../../components/icons"; export default function PerformanceSettings() { const { register, formState } = useFormContext(); - const cacheDetails = useDisclosure(); return ( @@ -107,6 +95,15 @@ export default function PerformanceSettings() { Enabled: show signature verification on notes + + + + Automatically decrypt DMs + + + + Enabled: automatically decrypt direct messages + diff --git a/src/views/signin/address/create.tsx b/src/views/signin/address/create.tsx index dbf8ea742..bc7cc2336 100644 --- a/src/views/signin/address/create.tsx +++ b/src/views/signin/address/create.tsx @@ -24,13 +24,13 @@ import { useNavigate } from "react-router-dom"; import { NostrEvent } from "nostr-tools"; import useNip05Providers from "../../../hooks/use-nip05-providers"; -import { Kind0ParsedContent } from "../../../helpers/user-metadata"; +import { Kind0ParsedContent } from "../../../helpers/nostr/user-metadata"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; -import { MetadataAvatar } from "../../../components/user-avatar"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; +import { MetadataAvatar } from "../../../components/user/user-avatar"; import { ErrorBoundary } from "../../../components/error-boundary"; import dnsIdentityService from "../../../services/dns-identity"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; import nostrConnectService from "../../../services/nostr-connect"; import accountService from "../../../services/account"; import { safeRelayUrls } from "../../../helpers/relay"; diff --git a/src/views/signin/address/index.tsx b/src/views/signin/address/index.tsx index 0000472a9..2eda5b702 100644 --- a/src/views/signin/address/index.tsx +++ b/src/views/signin/address/index.tsx @@ -10,7 +10,7 @@ import accountService from "../../../services/account"; import { COMMON_CONTACT_RELAY } from "../../../const"; import { safeRelayUrls } from "../../../helpers/relay"; import { getMatchSimpleEmail } from "../../../helpers/regexp"; -import QRCodeScannerButton from "../../../components/qr-code-scanner-button"; +import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button"; export default function LoginNostrAddressView() { const navigate = useNavigate(); diff --git a/src/views/signin/components/account-card.tsx b/src/views/signin/components/account-card.tsx index 0d28ef09a..0f2ac8fde 100644 --- a/src/views/signin/components/account-card.tsx +++ b/src/views/signin/components/account-card.tsx @@ -1,9 +1,9 @@ import { CloseIcon } from "@chakra-ui/icons"; import { Box, IconButton, Text } from "@chakra-ui/react"; -import { getUserDisplayName } from "../../../helpers/user-metadata"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../../helpers/nostr/user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; import accountService, { Account } from "../../../services/account"; -import UserAvatar from "../../../components/user-avatar"; +import UserAvatar from "../../../components/user/user-avatar"; import AccountInfoBadge from "../../../components/account-info-badge"; export default function AccountCard({ account }: { account: Account }) { diff --git a/src/views/signin/nostr-connect.tsx b/src/views/signin/nostr-connect.tsx index 7c161e97a..beff5959d 100644 --- a/src/views/signin/nostr-connect.tsx +++ b/src/views/signin/nostr-connect.tsx @@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"; import accountService from "../../services/account"; import nostrConnectService, { NostrConnectClient } from "../../services/nostr-connect"; -import QRCodeScannerButton from "../../components/qr-code-scanner-button"; +import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button"; export default function LoginNostrConnectView() { const navigate = useNavigate(); diff --git a/src/views/signin/npub.tsx b/src/views/signin/npub.tsx index 4c2d23459..23b6b69f4 100644 --- a/src/views/signin/npub.tsx +++ b/src/views/signin/npub.tsx @@ -6,7 +6,7 @@ import { RelayUrlInput } from "../../components/relay-url-input"; import { normalizeToHexPubkey } from "../../helpers/nip19"; import accountService from "../../services/account"; import { COMMON_CONTACT_RELAY } from "../../const"; -import QRCodeScannerButton from "../../components/qr-code-scanner-button"; +import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button"; export default function LoginNpubView() { const navigate = useNavigate(); diff --git a/src/views/signup/backup-step.tsx b/src/views/signup/backup-step.tsx index 7bf734b4b..88c3b5f21 100644 --- a/src/views/signup/backup-step.tsx +++ b/src/views/signup/backup-step.tsx @@ -98,7 +98,7 @@ export default function BackupStep({ secretKey, onConfirm }: { secretKey: string Secret Key - + This is the key to access your account, keep it secret. diff --git a/src/views/signup/create-step.tsx b/src/views/signup/create-step.tsx index 52c086685..9cd6f2f53 100644 --- a/src/views/signup/create-step.tsx +++ b/src/views/signup/create-step.tsx @@ -4,7 +4,7 @@ import { Avatar, Button, Flex, Heading, Text, useToast } from "@chakra-ui/react" import { bytesToHex } from "@noble/hashes/utils"; import dayjs from "dayjs"; -import { Kind0ParsedContent } from "../../helpers/user-metadata"; +import { Kind0ParsedContent } from "../../helpers/nostr/user-metadata"; import { containerProps } from "./common"; import { nostrBuildUploadImage } from "../../helpers/nostr-build"; import accountService from "../../services/account"; diff --git a/src/views/signup/finished-step.tsx b/src/views/signup/finished-step.tsx index ae0334f8b..c9a52b1f9 100644 --- a/src/views/signup/finished-step.tsx +++ b/src/views/signup/finished-step.tsx @@ -2,12 +2,12 @@ import { Box, Button, Card, Flex, Heading, Text } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { useAsync } from "react-use"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import { containerProps } from "./common"; -import { UserFollowButton } from "../../components/user-follow-button"; -import { Kind0ParsedContent } from "../../helpers/user-metadata"; -import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; +import { UserFollowButton } from "../../components/user/user-follow-button"; +import { Kind0ParsedContent } from "../../helpers/nostr/user-metadata"; +import { UserDnsIdentityIcon } from "../../components/user/user-dns-identity-icon"; type TrendingApi = { profiles: { diff --git a/src/views/signup/index.tsx b/src/views/signup/index.tsx index 2d0a1acb0..30b5daeba 100644 --- a/src/views/signup/index.tsx +++ b/src/views/signup/index.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Flex } from "@chakra-ui/react"; import { useNavigate, useParams } from "react-router-dom"; -import { Kind0ParsedContent } from "../../helpers/user-metadata"; +import { Kind0ParsedContent } from "../../helpers/nostr/user-metadata"; import NameStep from "./name-step"; import ProfileImageStep from "./profile-image-step"; import RelayStep from "./relay-step"; diff --git a/src/views/signup/name-step.tsx b/src/views/signup/name-step.tsx index 29f8f56cc..1212bb78c 100644 --- a/src/views/signup/name-step.tsx +++ b/src/views/signup/name-step.tsx @@ -2,7 +2,7 @@ import { Button, Flex, Heading, Input, Text, Textarea } from "@chakra-ui/react"; import { useForm } from "react-hook-form"; import { Link as RouterLink, useLocation } from "react-router-dom"; -import { Kind0ParsedContent } from "../../helpers/user-metadata"; +import { Kind0ParsedContent } from "../../helpers/nostr/user-metadata"; import { AppIcon, containerProps } from "./common"; export default function NameStep({ onSubmit }: { onSubmit: (metadata: Kind0ParsedContent) => void }) { diff --git a/src/views/streams/components/stream-card.tsx b/src/views/streams/components/stream-card.tsx index 4e7e4f260..29e30aab4 100644 --- a/src/views/streams/components/stream-card.tsx +++ b/src/views/streams/components/stream-card.tsx @@ -3,12 +3,12 @@ import { Box, Card, CardBody, CardProps, Flex, Heading, LinkBox, LinkOverlay, Te import { ParsedStream } from "../../../helpers/nostr/stream"; import { Link as RouterLink } from "react-router-dom"; -import UserAvatar from "../../../components/user-avatar"; -import UserLink from "../../../components/user-link"; +import UserAvatar from "../../../components/user/user-avatar"; +import UserLink from "../../../components/user/user-link"; import StreamStatusBadge from "./status-badge"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import useEventNaddr from "../../../hooks/use-event-naddr"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import StreamHashtags from "./stream-hashtags"; import Timestamp from "../../../components/timestamp"; diff --git a/src/views/streams/components/streamer-cards.tsx b/src/views/streams/components/streamer-cards.tsx index 89e960447..d067e0659 100644 --- a/src/views/streams/components/streamer-cards.tsx +++ b/src/views/streams/components/streamer-cards.tsx @@ -2,20 +2,20 @@ import { useMemo } from "react"; import { Card, CardBody, CardHeader, CardProps, Heading, Image, LinkBox, LinkOverlay } from "@chakra-ui/react"; import { useReadRelays } from "../../../hooks/use-client-relays"; -import replaceableEventLoaderService from "../../../services/replaceable-event-requester"; +import replaceableEventsService from "../../../services/replaceable-events"; import useSubject from "../../../hooks/use-subject"; -import { NoteContents } from "../../../components/note/text-note-contents"; import { isATag } from "../../../types/nostr-event"; import useReplaceableEvent from "../../../hooks/use-replaceable-event"; -import OpenGraphCard from "../../../components/open-graph-card"; +import OpenGraphCard from "../../../components/open-graph/open-graph-card"; import { useAdditionalRelayContext } from "../../../providers/local/additional-relay-context"; +import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents"; export const STREAMER_CARDS_TYPE = 17777; export const STREAMER_CARD_TYPE = 37777; function useStreamerCardsCords(pubkey: string, relays: Iterable) { const sub = useMemo( - () => replaceableEventLoaderService.requestEvent(relays, STREAMER_CARDS_TYPE, pubkey), + () => replaceableEventsService.requestEvent(relays, STREAMER_CARDS_TYPE, pubkey), [pubkey, relays], ); const streamerCards = useSubject(sub); @@ -48,7 +48,7 @@ function StreamerCard({ cord, relay, ...props }: { cord: string; relay?: string )} {card.content && ( - + )} {link && ( diff --git a/src/views/streams/components/top-zappers.tsx b/src/views/streams/components/top-zappers.tsx index 4f8586ac0..17fe86507 100644 --- a/src/views/streams/components/top-zappers.tsx +++ b/src/views/streams/components/top-zappers.tsx @@ -2,13 +2,13 @@ import { useMemo } from "react"; import { Flex, FlexProps, Text } from "@chakra-ui/react"; import { parseZapEvent } from "../../../helpers/nostr/zaps"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; import { LightningIcon } from "../../../components/icons"; import { readablizeSats } from "../../../helpers/bolt11"; import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline"; import { ParsedStream } from "../../../helpers/nostr/stream"; import useSubject from "../../../hooks/use-subject"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; export default function TopZappers({ stream, ...props }: FlexProps & { stream: ParsedStream }) { const timeline = useStreamChatTimeline(stream); diff --git a/src/views/streams/dashboard/index.tsx b/src/views/streams/dashboard/index.tsx index 343904dad..a2ba627e8 100644 --- a/src/views/streams/dashboard/index.tsx +++ b/src/views/streams/dashboard/index.tsx @@ -12,7 +12,7 @@ import { ParsedStream, STREAM_KIND, getATag } from "../../../helpers/nostr/strea import useTimelineLoader from "../../../hooks/use-timeline-loader"; import RequireCurrentAccount from "../../../providers/route/require-current-account"; import useCurrentAccount from "../../../hooks/use-current-account"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { ChevronLeftIcon } from "../../../components/icons"; import { AdditionalRelayProvider } from "../../../providers/local/additional-relay-context"; diff --git a/src/views/streams/dashboard/users-card.tsx b/src/views/streams/dashboard/users-card.tsx index 2d2268bb2..11b1acdf9 100644 --- a/src/views/streams/dashboard/users-card.tsx +++ b/src/views/streams/dashboard/users-card.tsx @@ -1,18 +1,17 @@ import { ReactNode, memo, useMemo, useState } from "react"; import { Button, ButtonGroup, Divider, Flex, Heading } from "@chakra-ui/react"; import dayjs from "dayjs"; +import { useInterval, useObservable } from "react-use"; -import useSubject from "../../../hooks/use-subject"; import useCurrentAccount from "../../../hooks/use-current-account"; import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline"; -import UserAvatar from "../../../components/user-avatar"; -import UserLink from "../../../components/user-link"; +import UserAvatar from "../../../components/user/user-avatar"; +import UserLink from "../../../components/user/user-link"; import useUserMuteActions from "../../../hooks/use-user-mute-actions"; import { useMuteModalContext } from "../../../providers/route/mute-modal-provider"; import useUserMuteList from "../../../hooks/use-user-mute-list"; import { isPubkeyInList } from "../../../helpers/nostr/lists"; import { ParsedStream } from "../../../helpers/nostr/stream"; -import { useInterval } from "react-use"; function Countdown({ time }: { time: number }) { const [now, setNow] = useState(dayjs().unix()); @@ -61,7 +60,7 @@ function UsersCard({ stream }: { stream: ParsedStream }) { const streamChatTimeline = useStreamChatTimeline(stream); // refresh when a new event - useSubject(streamChatTimeline.events.onEvent); + useObservable(streamChatTimeline.events.onEvent); const chatEvents = streamChatTimeline.events.getSortedEvents(); const muteList = useUserMuteList(account.pubkey); diff --git a/src/views/streams/dashboard/zaps-card.tsx b/src/views/streams/dashboard/zaps-card.tsx index aad1449fb..51c11d77e 100644 --- a/src/views/streams/dashboard/zaps-card.tsx +++ b/src/views/streams/dashboard/zaps-card.tsx @@ -1,8 +1,8 @@ import { memo } from "react"; import { Flex } from "@chakra-ui/react"; import { kinds } from "nostr-tools"; +import { useObservable } from "react-use"; -import useSubject from "../../../hooks/use-subject"; import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline"; import ZapMessageMemo from "../stream/stream-chat/zap-message"; import { ParsedStream } from "../../../helpers/nostr/stream"; @@ -11,7 +11,7 @@ function ZapsCard({ stream }: { stream: ParsedStream }) { const streamChatTimeline = useStreamChatTimeline(stream); // refresh when a new event - useSubject(streamChatTimeline.events.onEvent); + useObservable(streamChatTimeline.events.onEvent); const zapMessages = streamChatTimeline.events.getSortedEvents().filter((event) => { if (stream.starts && event.created_at < stream.starts) return false; if (stream.ends && event.created_at > stream.ends) return false; diff --git a/src/views/streams/index.tsx b/src/views/streams/index.tsx index 12d1c8921..57627d7b5 100644 --- a/src/views/streams/index.tsx +++ b/src/views/streams/index.tsx @@ -12,7 +12,7 @@ import PeopleListSelection from "../../components/people-list-selection/people-l import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import useParsedStreams from "../../hooks/use-parsed-streams"; -import { NostrRequestFilter } from "../../types/nostr-query"; +import { NostrRequestFilter } from "../../types/nostr-relay"; import { useAppTitle } from "../../hooks/use-app-title"; import { NostrEvent } from "../../types/nostr-event"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/streams/stream/index.tsx b/src/views/streams/stream/index.tsx index ba0e75a3f..ab1b3ee50 100644 --- a/src/views/streams/stream/index.tsx +++ b/src/views/streams/stream/index.tsx @@ -25,13 +25,13 @@ import { useReadRelays } from "../../../hooks/use-client-relays"; import { unique } from "../../../helpers/array"; import { LiveVideoPlayer } from "../../../components/live-video-player"; import StreamChat, { ChatDisplayMode } from "./stream-chat"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import StreamSummaryContent from "../components/stream-summary-content"; import { ChevronLeftIcon, ExternalLinkIcon } from "../../../components/icons"; import useSetColorMode from "../../../hooks/use-set-color-mode"; import { CopyIconButton } from "../../../components/copy-icon-button"; -import replaceableEventLoaderService from "../../../services/replaceable-event-requester"; +import replaceableEventsService from "../../../services/replaceable-events"; import useSubject from "../../../hooks/use-subject"; import StreamerCards from "../components/streamer-cards"; import { useAppTitle } from "../../../hooks/use-app-title"; @@ -60,7 +60,7 @@ function DesktopStreamPage({ stream }: { stream: ParsedStream }) { return ( 0 && ( - + )} diff --git a/src/views/thread/components/thread-post.tsx b/src/views/thread/components/thread-post.tsx index 1d0cbd145..823bcccc7 100644 --- a/src/views/thread/components/thread-post.tsx +++ b/src/views/thread/components/thread-post.tsx @@ -1,46 +1,35 @@ import { memo, useRef, useState } from "react"; -import { - Alert, - AlertIcon, - Button, - ButtonGroup, - Flex, - IconButton, - Link, - Spacer, - useColorMode, - useDisclosure, -} from "@chakra-ui/react"; +import { Alert, AlertIcon, Button, ButtonGroup, Flex, IconButton, Link, Spacer, useDisclosure } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; +import ReplyForm from "./reply-form"; import { ReplyIcon } from "../../../components/icons"; import { countReplies, ThreadItem } from "../../../helpers/thread"; import { TrustProvider } from "../../../providers/local/trust"; -import ReplyForm from "./reply-form"; import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import Timestamp from "../../../components/timestamp"; -import { NoteContents } from "../../../components/note/text-note-contents"; import Expand01 from "../../../components/icons/expand-01"; import Minus from "../../../components/icons/minus"; -import NoteZapButton from "../../../components/note/note-zap-button"; -import QuoteRepostButton from "../../../components/note/components/quote-repost-button"; -import RepostButton from "../../../components/note/components/repost-button"; -import NoteMenu from "../../../components/note/note-menu"; import { useBreakpointValue } from "../../../providers/global/breakpoint-provider"; -import NoteReactions from "../../../components/note/components/note-reactions"; -import BookmarkButton from "../../../components/note/components/bookmark-button"; -import NoteCommunityMetadata from "../../../components/note/note-community-metadata"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; -import NoteProxyLink from "../../../components/note/components/note-proxy-link"; -import { NoteDetailsButton } from "../../../components/note/components/note-details-button"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import EventInteractionDetailsModal from "../../../components/event-interactions-modal"; import { getSharableEventAddress } from "../../../helpers/nip19"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import useAppSettings from "../../../hooks/use-app-settings"; import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props"; -import POWIcon from "../../../components/pow-icon"; +import POWIcon from "../../../components/pow/pow-icon"; +import RepostButton from "../../../components/note/timeline-note/components/repost-button"; +import QuoteRepostButton from "../../../components/note/quote-repost-button"; +import NoteZapButton from "../../../components/note/note-zap-button"; +import NoteProxyLink from "../../../components/note/timeline-note/components/note-proxy-link"; +import { NoteDetailsButton } from "../../../components/note/timeline-note/components/note-details-button"; +import BookmarkButton from "../../../components/note/bookmark-button"; +import NoteMenu from "../../../components/note/note-menu"; +import NoteCommunityMetadata from "../../../components/note/timeline-note/note-community-metadata"; +import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents"; +import NoteReactions from "../../../components/note/timeline-note/components/note-reactions"; export type ThreadItemProps = { post: ThreadItem; @@ -106,7 +95,7 @@ export const ThreadPost = memo(({ post, initShowReplies, focusId, level = -1 }: <> - + ); diff --git a/src/views/thread/index.tsx b/src/views/thread/index.tsx index a55553151..b0bb2dff8 100644 --- a/src/views/thread/index.tsx +++ b/src/views/thread/index.tsx @@ -3,7 +3,6 @@ import { Card, Heading, Link, Spinner } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import Note from "../../components/note"; import { ThreadPost } from "./components/thread-post"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { useReadRelays } from "../../hooks/use-client-relays"; @@ -14,10 +13,11 @@ import useThreadTimelineLoader from "../../hooks/use-thread-timeline-loader"; import useSingleEvent from "../../hooks/use-single-event"; import useParamsEventPointer from "../../hooks/use-params-event-pointer"; import LoadingNostrLink from "../../components/loading-nostr-link"; -import UserName from "../../components/user-name"; +import UserName from "../../components/user/user-name"; import { getSharableEventAddress } from "../../helpers/nip19"; -import UserAvatarLink from "../../components/user-avatar-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; import { ReplyIcon } from "../../components/icons"; +import TimelineNote from "../../components/note/timeline-note"; function CollapsedReplies({ pointer, @@ -90,7 +90,7 @@ function ThreadPage({ )} {focusedPost.replyingTo ? ( - + ) : ( focusedPost.refs.reply?.e && )} diff --git a/src/views/tools/event-console/event-row.tsx b/src/views/tools/event-console/event-row.tsx new file mode 100644 index 000000000..e73d87db0 --- /dev/null +++ b/src/views/tools/event-console/event-row.tsx @@ -0,0 +1,99 @@ +import { Box, Code, Flex, Heading, Switch, Text, useDisclosure } from "@chakra-ui/react"; +import { NostrEvent } from "nostr-tools"; +import ExpandButton from "./expand-button"; +import UserName from "../../../components/user/user-name"; +import { CopyIconButton } from "../../../components/copy-icon-button"; +import Timestamp from "../../../components/timestamp"; +import stringify from "json-stringify-deterministic"; + +export default function EventRow({ event }: { event: NostrEvent }) { + const expanded = useDisclosure(); + const raw = useDisclosure(); + + return ( + <> + + + {event.id.slice(0, 8)} ({event.kind}) [{event.tags.length}] + + + {!expanded.isOpen && ( + + {event.content} + + )} + + + + {expanded.isOpen && ( + + + {raw.isOpen && ( + + )} + + Raw + + + {raw.isOpen ? ( + + {stringify(event, { space: " " })} + + ) : ( + <> + + ID: + {event.id} + + + + Pubkey: + {event.pubkey} + + + + Created: + {event.created_at} + + + + {event.content} + + + Tags: + + {event.tags.map((t, i) => ( + + {t.map((v, ii) => ( + + {v} + + ))} + + ))} + + + + )} + + )} + + ); +} diff --git a/src/views/tools/event-console/expand-button.tsx b/src/views/tools/event-console/expand-button.tsx new file mode 100644 index 000000000..2962008e5 --- /dev/null +++ b/src/views/tools/event-console/expand-button.tsx @@ -0,0 +1,18 @@ +import { IconButton, IconButtonProps } from "@chakra-ui/react"; +import { ChevronDownIcon, ChevronUpIcon } from "../../../components/icons"; + +export default function ExpandButton({ + isOpen, + onToggle, + ...props +}: { isOpen: boolean; onToggle: () => void } & Omit) { + return ( + : } + aria-label={isOpen ? "Collapse" : "Expand"} + title={isOpen ? "Collapse" : "Expand"} + {...props} + /> + ); +} diff --git a/src/views/tools/event-console/filter-editor.tsx b/src/views/tools/event-console/filter-editor.tsx new file mode 100644 index 000000000..67a480265 --- /dev/null +++ b/src/views/tools/event-console/filter-editor.tsx @@ -0,0 +1,64 @@ +import { memo, useMemo } from "react"; +import { useColorMode } from "@chakra-ui/react"; +import ReactCodeMirror from "@uiw/react-codemirror"; +import { githubLight, githubDark } from "@uiw/codemirror-theme-github"; +import { jsonSchema } from "codemirror-json-schema"; +import { keymap } from "@codemirror/view"; +import { useInterval } from "react-use"; +import _throttle from "lodash.throttle"; +import { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; +import { jsonLanguage } from "@codemirror/lang-json"; +import { syntaxTree } from "@codemirror/language"; + +import { NostrFilterSchema } from "./schema"; +import { UserDirectory, useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider"; +import { codeMirrorUserAutocomplete, updateCodeMirrorUserAutocomplete } from "./user-autocomplete"; + +const FilterEditor = memo( + ({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun: () => void }) => { + const getDirectory = useUserSearchDirectoryContext(); + const { colorMode } = useColorMode(); + + useInterval(() => { + updateCodeMirrorUserAutocomplete(getDirectory()); + }, 1000); + + const extensions = useMemo( + () => [ + keymap.of([ + { + win: "Ctrl-Enter", + linux: "Ctrl-Enter", + mac: "Cmd-Enter", + preventDefault: true, + run: () => { + onRun(); + return true; + }, + shift: () => { + onRun(); + return true; + }, + }, + ]), + jsonSchema(NostrFilterSchema), + jsonLanguage.data.of({ + autocomplete: codeMirrorUserAutocomplete, + }), + ], + [onRun], + ); + return ( + + ); + }, +); + +export default FilterEditor; diff --git a/src/views/tools/event-console/help-modal.tsx b/src/views/tools/event-console/help-modal.tsx new file mode 100644 index 000000000..dac1d85d5 --- /dev/null +++ b/src/views/tools/event-console/help-modal.tsx @@ -0,0 +1,51 @@ +import { + Code, + Flex, + Heading, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + ModalProps, + Text, +} from "@chakra-ui/react"; + +export default function HelpModal({ isOpen, onClose, ...props }: Omit) { + return ( + + + + Help + + + Keyboard shortcuts + + Ctrl+Shift+Enter: Run Filter + + + + Pubkeys + + + Typing @ inside any string will autocomplete with a list of users the app has seen + + + + Dates + + + since and until fields can both take relative times in the form of strings + + Examples: + + {["now", "n-3h", "n-5", "n+4m", "n-7d", "n-30s", "n-4w"].map((t) => ( + {t} + ))} + + + + + ); +} diff --git a/src/views/tools/event-console/history-drawer.tsx b/src/views/tools/event-console/history-drawer.tsx new file mode 100644 index 000000000..f6f54d67a --- /dev/null +++ b/src/views/tools/event-console/history-drawer.tsx @@ -0,0 +1,84 @@ +import { useRef, useState } from "react"; +import { + Button, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + DrawerProps, + Flex, + Input, +} from "@chakra-ui/react"; +import ClockRewind from "../../../components/icons/clock-rewind"; +import { CopyIconButton } from "../../../components/copy-icon-button"; + +type HistoryDrawerProps = Omit & { + history: string[]; + onClear: () => void; + onSelect: (item: string) => void; +}; + +export default function HistoryDrawer({ onClose, isOpen, history, onClear, onSelect }: HistoryDrawerProps) { + const searchRef = useRef(null); + const [search, setSearch] = useState(""); + + const filteredHistory = search.length > 1 ? history.filter((query) => query.includes(search)) : history; + + return ( + + + + + + History + + + + setSearch(e.target.value)} + /> + + + {filteredHistory.map((query) => ( + + + + + ))} + + + + + + + + + ); +} diff --git a/src/views/tools/event-console/index.tsx b/src/views/tools/event-console/index.tsx new file mode 100644 index 000000000..bd7651816 --- /dev/null +++ b/src/views/tools/event-console/index.tsx @@ -0,0 +1,205 @@ +import { memo, useCallback, useRef, useState } from "react"; +import { + Alert, + AlertDescription, + AlertIcon, + AlertTitle, + Box, + Button, + ButtonGroup, + CloseButton, + Flex, + Heading, + IconButton, + Switch, + Text, + useDisclosure, +} from "@chakra-ui/react"; +import { NostrEvent, Relay, Subscription } from "nostr-tools"; +import { useLocalStorage } from "react-use"; +import { Subscription as IDBSubscription, CacheRelay } from "nostr-idb"; +import _throttle from "lodash.throttle"; + +import VerticalPageLayout from "../../../components/vertical-page-layout"; +import BackButton from "../../../components/router/back-button"; +import { localRelay } from "../../../services/local-relay"; +import Play from "../../../components/icons/play"; +import ClockRewind from "../../../components/icons/clock-rewind"; +import HistoryDrawer from "./history-drawer"; +import EventRow from "./event-row"; +import { processFilter } from "./process"; +import HelpModal from "./help-modal"; +import HelpCircle from "../../../components/icons/help-circle"; +import stringify from "json-stringify-deterministic"; +import { DownloadIcon } from "../../../components/icons"; +import { RelayUrlInput } from "../../../components/relay-url-input"; +import { validateRelayURL } from "../../../helpers/relay"; +import FilterEditor from "./filter-editor"; + +const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => { + return ( + <> + {events.map((event) => ( + + ))} + + ); +}); + +export default function EventConsoleView() { + const historyDrawer = useDisclosure(); + const [history, setHistory] = useLocalStorage("console-history", []); + const helpModal = useDisclosure(); + const queryRelay = useDisclosure(); + const [relayURL, setRelayURL] = useState(""); + const [relay, setRelay] = useState(null); + + const [sub, setSub] = useState(null); + + const [query, setQuery] = useState(() => history?.[0] || JSON.stringify({ kinds: [1], limit: 20 }, null, 2)); + + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [events, setEvents] = useState([]); + const loadEvents = useCallback(async () => { + try { + if (queryRelay.isOpen && !relayURL) throw new Error("Must set relay"); + + const filter = await processFilter(JSON.parse(query)); + setLoading(true); + setHistory((arr) => (arr ? (!arr.includes(query) ? [query, ...arr] : arr) : [query])); + setEvents([]); + + if (sub) sub.close(); + + let r: Relay | CacheRelay = localRelay; + if (queryRelay.isOpen) { + const url = validateRelayURL(relayURL); + if (!relay || relay.url !== url.toString()) { + if (relay) relay.close(); + r = new Relay(url.toString()); + await r.connect(); + setRelay(r); + } else r = relay; + } else { + if (relay) { + relay.close(); + setRelay(null); + } + } + + await new Promise((res) => { + let buffer: NostrEvent[] = []; + const flush = _throttle(() => setEvents([...buffer]), 1000 / 10, { trailing: true }); + + const s = r.subscribe([filter], { + onevent: (e) => { + buffer.push(e); + flush(); + }, + oneose: () => { + setEvents([...buffer]); + res(); + }, + }); + setSub(s); + }); + } catch (e) { + if (e instanceof Error) setError(e.message); + } + setLoading(false); + }, [queryRelay.isOpen, query, relayURL, relay, sub]); + + const submitRef = useRef(loadEvents); + submitRef.current = loadEvents; + + const submitCode = useCallback(() => submitRef.current(), []); + + const downloadEvents = () => { + const lines = events.map((e) => stringify(e)).join("\n"); + const file = new File([lines], "events.json", { type: "application/jsonl" }); + const url = URL.createObjectURL(file); + window.open(url, "_blank"); + }; + + return ( + + + + Event Console + + Query Relay + + {queryRelay.isOpen && ( + setRelayURL(e.target.value)} + /> + )} + + } aria-label="Help" title="Help" size="sm" onClick={helpModal.onOpen} /> + } + aria-label="History" + title="History" + size="sm" + onClick={historyDrawer.onOpen} + /> + + + + + + + {error && ( + + + + Error + {error} + + setError("")} /> + + )} + + + {events.length} events + {sub && ( + + Subscribed + + )} + {events.length > 0 && ( + } + onClick={downloadEvents} + size="xs" + /> + )} + + + + + + setHistory([])} + onSelect={(v) => { + setQuery(v); + historyDrawer.onClose(); + }} + /> + + + + ); +} diff --git a/src/views/tools/event-console/process.ts b/src/views/tools/event-console/process.ts new file mode 100644 index 000000000..f6f8c9466 --- /dev/null +++ b/src/views/tools/event-console/process.ts @@ -0,0 +1,48 @@ +import dayjs from "dayjs"; +import { Filter, nip19 } from "nostr-tools"; + +export function processDateString(date: string) { + if (date.toLowerCase() === "now" || date.toLowerCase() === "n") { + return dayjs().unix(); + } else if (date.startsWith("n")) { + const match = date.match(/n([+-])(\d+)([hwmsd])?/i); + if (match === null) throw new Error(`Cant parse relative date string ${date}`); + + if (match[1] === "-") { + return ( + dayjs() + // @ts-expect-error + .subtract(parseInt(match[2]), match[3] || "h") + .unix() + ); + } else if (match[1] === "+") { + return ( + dayjs() + // @ts-expect-error + .add(parseInt(match[2]), match[3] || "h") + .unix() + ); + } else throw Error(`Unknown operation ${match[1]}`); + } + + throw new Error(`Unknown date string ${date}`); +} + +export async function processFilter(f: Filter): Promise { + const filter = JSON.parse(JSON.stringify(f)) as Filter; + + if (filter.authors) + filter.authors = filter.authors.map((p) => { + if (p.startsWith("npub")) return nip19.decode(p).data as string; + return p; + }); + + if (typeof filter.since === "string") { + filter.since = processDateString(filter.since); + } + if (typeof filter.until === "string") { + filter.until = processDateString(filter.until); + } + + return filter; +} diff --git a/src/views/tools/event-console/schema.ts b/src/views/tools/event-console/schema.ts new file mode 100644 index 000000000..6dfb636b9 --- /dev/null +++ b/src/views/tools/event-console/schema.ts @@ -0,0 +1,75 @@ +import { type JSONSchema7 } from "json-schema"; +import { kinds } from "nostr-tools"; + +const kindNumbers = Object.values(kinds).filter((t) => typeof t === "number") as number[]; + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); +export const NostrFilterSchema: JSONSchema7 = { + type: "object", + properties: { + ids: { + type: "array", + minItems: 1, + uniqueItems: true, + items: { + type: "string", + }, + }, + kinds: { + type: "array", + description: "Filter by event kind", + minItems: 1, + uniqueItems: true, + items: { + type: "integer", + minimum: 0, + examples: kindNumbers, + }, + }, + authors: { + type: "array", + description: "Filter by pubkey", + minItems: 1, + uniqueItems: true, + items: { + type: "string", + }, + }, + limit: { + type: "integer", + description: "max number of events to return", + default: 20, + minimum: 0, + }, + until: { + description: "Return events before or on this date", + oneOf: [ + { + type: "integer", + minimum: 0, + }, + { type: "string" }, + ], + }, + since: { + description: "Return events after or on this date", + oneOf: [ + { + type: "integer", + minimum: 0, + }, + { type: "string" }, + ], + }, + }, +}; + +for (const letter of letters) { + NostrFilterSchema.properties!["#" + letter] = { + type: "array", + description: `Filter on ${letter} tag`, + items: { + type: "string", + }, + }; +} diff --git a/src/views/tools/event-console/user-autocomplete.ts b/src/views/tools/event-console/user-autocomplete.ts new file mode 100644 index 000000000..7acd38012 --- /dev/null +++ b/src/views/tools/event-console/user-autocomplete.ts @@ -0,0 +1,33 @@ +import _throttle from "lodash.throttle"; +import { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; +import { syntaxTree } from "@codemirror/language"; + +import { UserDirectory } from "../../../providers/global/user-directory-provider"; + +let users: UserDirectory = []; +export function codeMirrorUserAutocomplete(context: CompletionContext): CompletionResult | null { + let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1); + if (nodeBefore.name !== "String") return null; + + let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos); + let tagBefore = /@\w*$/.exec(textBefore); + if (!tagBefore && !context.explicit) return null; + + return { + from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos, + validFor: /^(@\w*)?$/, + // options: tagOptions, + options: users + .filter((u) => !!u.names[0]) + .map((user) => ({ + label: "@" + user.names[0]!, + type: "keyword", + apply: user.pubkey, + detail: "pubkey", + })), + }; +} + +export function updateCodeMirrorUserAutocomplete(newUsers: UserDirectory) { + users = newUsers; +} diff --git a/src/views/tools/event-publisher/event-editor.tsx b/src/views/tools/event-publisher/event-editor.tsx new file mode 100644 index 000000000..d2af50107 --- /dev/null +++ b/src/views/tools/event-publisher/event-editor.tsx @@ -0,0 +1,62 @@ +import { memo, useMemo } from "react"; +import { useColorMode } from "@chakra-ui/react"; +import ReactCodeMirror from "@uiw/react-codemirror"; +import { githubLight, githubDark } from "@uiw/codemirror-theme-github"; +import { jsonSchema } from "codemirror-json-schema"; +import { keymap } from "@codemirror/view"; +import { useInterval } from "react-use"; +import _throttle from "lodash.throttle"; +import { jsonLanguage } from "@codemirror/lang-json"; + +import { NostrEventSchema } from "./schema"; +import { useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider"; +import { updateCodeMirrorUserAutocomplete, codeMirrorUserAutocomplete } from "../event-console/user-autocomplete"; + +const EventEditor = memo( + ({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun?: () => void }) => { + const getDirectory = useUserSearchDirectoryContext(); + const { colorMode } = useColorMode(); + + useInterval(() => { + updateCodeMirrorUserAutocomplete(getDirectory()); + }, 1000); + + const extensions = useMemo( + () => [ + keymap.of([ + { + win: "Ctrl-Enter", + linux: "Ctrl-Enter", + mac: "Cmd-Enter", + preventDefault: true, + run: () => { + if (onRun) onRun(); + return true; + }, + shift: () => { + if (onRun) onRun(); + return true; + }, + }, + ]), + jsonSchema(NostrEventSchema), + jsonLanguage.data.of({ + autocomplete: codeMirrorUserAutocomplete, + }), + ], + [onRun], + ); + return ( + + ); + }, +); + +export default EventEditor; diff --git a/src/views/tools/event-publisher/index.tsx b/src/views/tools/event-publisher/index.tsx new file mode 100644 index 000000000..489681cc0 --- /dev/null +++ b/src/views/tools/event-publisher/index.tsx @@ -0,0 +1,170 @@ +import { useMemo, useState } from "react"; +import { + Button, + ButtonGroup, + Code, + Flex, + Heading, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useToast, +} from "@chakra-ui/react"; +import VerticalPageLayout from "../../../components/vertical-page-layout"; +import BackButton from "../../../components/router/back-button"; +import Play from "../../../components/icons/play"; +import EventEditor from "./event-editor"; +import { EventTemplate, NostrEvent, UnsignedEvent, getEventHash, verifyEvent } from "nostr-tools"; +import dayjs from "dayjs"; +import { processEvent } from "./process"; +import { WritingIcon } from "../../../components/icons"; +import { useSigningContext } from "../../../providers/global/signing-provider"; +import { usePublishEvent } from "../../../providers/global/publish-provider"; +import useCurrentAccount from "../../../hooks/use-current-account"; +import UserAvatar from "../../../components/user/user-avatar"; + +export default function EventPublisherView() { + const toast = useToast(); + const [loading, setLoading] = useState(false); + const { requestSignature } = useSigningContext(); + const publish = usePublishEvent(); + const account = useCurrentAccount(); + + const defaultEvent = useMemo( + () => + JSON.stringify( + { kind: 1234, content: "", tags: [], created_at: dayjs().unix() } satisfies EventTemplate, + null, + 2, + ), + [], + ); + const [value, setValue] = useState(defaultEvent); + const [draft, setDraft] = useState(); + + const submitEvent = () => { + try { + const draft = processEvent(JSON.parse(value) as UnsignedEvent); + if (account) draft.pubkey = account.pubkey; + (draft as NostrEvent).id = getEventHash(draft); + setDraft(draft); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + }; + + const sign = async () => { + if (!draft) return; + try { + setDraft(await requestSignature(draft)); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + }; + + const publishDraft = async () => { + if (!draft || !(draft as NostrEvent).sig) return; + try { + setLoading(true); + const valid = verifyEvent(draft as NostrEvent); + if (!valid) throw new Error("Invalid event"); + await publish("Custom Event", draft); + setDraft(undefined); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + setLoading(false); + }; + + const yolo = async () => { + try { + setLoading(true); + const draft = processEvent(JSON.parse(value) as UnsignedEvent); + if (account) draft.pubkey = account.pubkey; + (draft as NostrEvent).id = getEventHash(draft); + const event = await requestSignature(draft); + const valid = verifyEvent(event); + if (!valid) throw new Error("Invalid event"); + await publish("Custom Event", event); + setDraft(undefined); + } catch (e) { + if (e instanceof Error) toast({ description: e.message, status: "error" }); + } + setLoading(false); + }; + + return ( + <> + + + + Event Publisher + + {/* } aria-label="Help" title="Help" size="sm" onClick={helpModal.onOpen} /> */} + + + + + setValue(v)} onRun={submitEvent} /> + + {draft && ( + setDraft(undefined)} size="2xl"> + + + Publish Event + + + + 1. Event ID + + + {(draft as NostrEvent).id} + + + 2. Pubkey + + + + {(draft as NostrEvent).pubkey} + + + + + 3. Signature + + + + {(draft as NostrEvent).sig} + + + + + {(draft as NostrEvent).sig && ( + + )} + + + + + + + + + )} + + ); +} diff --git a/src/views/tools/event-publisher/process.ts b/src/views/tools/event-publisher/process.ts new file mode 100644 index 000000000..82b965919 --- /dev/null +++ b/src/views/tools/event-publisher/process.ts @@ -0,0 +1,10 @@ +import { EventTemplate, UnsignedEvent } from "nostr-tools"; +import { processDateString } from "../event-console/process"; + +export function processEvent(event: UnsignedEvent): UnsignedEvent { + if (typeof event.created_at === "string") { + event.created_at = processDateString(event.created_at); + } + + return event; +} diff --git a/src/views/tools/event-publisher/schema.ts b/src/views/tools/event-publisher/schema.ts new file mode 100644 index 000000000..7880b529f --- /dev/null +++ b/src/views/tools/event-publisher/schema.ts @@ -0,0 +1,44 @@ +import dayjs from "dayjs"; +import { type JSONSchema7 } from "json-schema"; + +export const NostrEventSchema: JSONSchema7 = { + type: "object", + required: ["kind", "created_at", "tags", "content"], + properties: { + id: { + type: "string", + description: "The id of the event", + }, + kind: { + type: "integer", + description: "The kind of event", + minimum: 0, + }, + pubkey: { + type: "string", + description: "The owner of the event", + }, + created_at: { + description: "The unix timestamp the event was created at", + oneOf: [ + { + type: "integer", + minimum: 0, + default: dayjs().unix(), + }, + { type: "string" }, + ], + }, + tags: { + type: "array", + description: "Event metadata tags", + items: { + type: "array", + minItems: 1, + items: { + type: "string", + }, + }, + }, + }, +}; diff --git a/src/views/tools/network-dm-graph.tsx b/src/views/tools/network-dm-graph.tsx index 5b97fb31c..56dcf9e1b 100644 --- a/src/views/tools/network-dm-graph.tsx +++ b/src/views/tools/network-dm-graph.tsx @@ -5,6 +5,7 @@ import ForceGraph, { LinkObject, NodeObject } from "react-force-graph-3d"; import { kinds } from "nostr-tools"; import dayjs from "dayjs"; import { useNavigate } from "react-router-dom"; +import { useDebounce, useObservable } from "react-use"; import { Group, Mesh, @@ -21,12 +22,10 @@ import RequireCurrentAccount from "../../providers/route/require-current-account import { useUsersMetadata } from "../../hooks/use-user-network"; import { getPubkeysFromList } from "../../helpers/nostr/lists"; import useUserContactList from "../../hooks/use-user-contact-list"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; import EventStore from "../../classes/event-store"; import NostrRequest from "../../classes/nostr-request"; import { isPTag } from "../../types/nostr-event"; -import { useDebounce } from "react-use"; -import useSubject from "../../hooks/use-subject"; import { ChevronLeftIcon } from "../../components/icons"; import { useReadRelays } from "../../hooks/use-client-relays"; @@ -53,7 +52,7 @@ function NetworkDMGraphPage() { store.clear(); const request = new NostrRequest(relays); - request.onEvent.subscribe(store.addEvent, store); + request.onEvent.subscribe((e) => store.addEvent(e)); request.start({ authors: contactsPubkeys, kinds: [kinds.EncryptedDirectMessage], @@ -71,7 +70,7 @@ function NetworkDMGraphPage() { const selfMetadata = useUserMetadata(account.pubkey); const usersMetadata = useUsersMetadata(contactsPubkeys); - const newEventTrigger = useSubject(store.onEvent); + const newEventTrigger = useObservable(store.onEvent); const graphData = useMemo(() => { if (store.events.size === 0) return { nodes: [], links: [] }; diff --git a/src/views/tools/network-mute-graph.tsx b/src/views/tools/network-mute-graph.tsx index d4d756c31..3863a1279 100644 --- a/src/views/tools/network-mute-graph.tsx +++ b/src/views/tools/network-mute-graph.tsx @@ -19,16 +19,16 @@ import { useUsersMetadata } from "../../hooks/use-user-network"; import { MUTE_LIST_KIND, getPubkeysFromList, isPubkeyInList } from "../../helpers/nostr/lists"; import useUserContactList from "../../hooks/use-user-contact-list"; import { useReadRelays } from "../../hooks/use-client-relays"; -import replaceableEventLoaderService from "../../services/replaceable-event-requester"; +import replaceableEventsService from "../../services/replaceable-events"; import useSubjects from "../../hooks/use-subjects"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; import { useNavigate } from "react-router-dom"; import { ChevronLeftIcon } from "../../components/icons"; export function useUsersMuteLists(pubkeys: string[], additionalRelays?: Iterable) { const readRelays = useReadRelays(additionalRelays); const muteListSubjects = useMemo(() => { - return pubkeys.map((pubkey) => replaceableEventLoaderService.requestEvent(readRelays, MUTE_LIST_KIND, pubkey)); + return pubkeys.map((pubkey) => replaceableEventsService.requestEvent(readRelays, MUTE_LIST_KIND, pubkey)); }, [pubkeys]); return useSubjects(muteListSubjects); } diff --git a/src/views/tools/satellite-cdn/index.tsx b/src/views/tools/satellite-cdn/index.tsx index 60b015421..10b7d2fbe 100644 --- a/src/views/tools/satellite-cdn/index.tsx +++ b/src/views/tools/satellite-cdn/index.tsx @@ -75,7 +75,7 @@ function FileRow({ file }: { file: SatelliteCDNFile }) { {file.name} - + {formatBytes(file.size)} {file.type} diff --git a/src/views/tools/transform-note/index.tsx b/src/views/tools/transform-note/index.tsx index a526fbfa0..62eb82083 100644 --- a/src/views/tools/transform-note/index.tsx +++ b/src/views/tools/transform-note/index.tsx @@ -4,13 +4,13 @@ import useParamsEventPointer from "../../../hooks/use-params-event-pointer"; import { NostrEvent } from "../../../types/nostr-event"; import useSingleEvent from "../../../hooks/use-single-event"; import { NoteTranslationsPage } from "./translation"; -import { NoteContents } from "../../../components/note/text-note-contents"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; -import UserLink from "../../../components/user-link"; -import NoteMenu from "../../../components/note/note-menu"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; +import UserLink from "../../../components/user/user-link"; import NoteTextToSpeechPage from "./text-to-speech"; import useRouteSearchValue from "../../../hooks/use-route-search-value"; +import NoteMenu from "../../../components/note/note-menu"; +import TextNoteContents from "../../../components/note/timeline-note/text-note-contents"; const tabs = ["original", "translation", "tts"]; @@ -41,7 +41,7 @@ function TransformNotePage({ note }: { note: NostrEvent }) { - + diff --git a/src/views/tools/transform-note/text-to-speech/index.tsx b/src/views/tools/transform-note/text-to-speech/index.tsx index 981fb4b6f..144f0081c 100644 --- a/src/views/tools/transform-note/text-to-speech/index.tsx +++ b/src/views/tools/transform-note/text-to-speech/index.tsx @@ -6,7 +6,7 @@ import { Filter } from "nostr-tools"; import { useReadRelays } from "../../../../hooks/use-client-relays"; import useTimelineLoader from "../../../../hooks/use-timeline-loader"; -import { getEventUID } from "../../../../helpers/nostr/events"; +import { getEventUID } from "../../../../helpers/nostr/event"; import { DVM_STATUS_KIND, DVM_TTS_JOB_KIND, @@ -40,7 +40,7 @@ export default function NoteTextToSpeechPage({ note }: { note: NostrEvent }) { }; await publish("Request Reading", draft); - }, [publish, note, readRelays]); + }, [publish, note, readRelays, lang]); const timeline = useTimelineLoader( `${getEventUID(note)}-readings`, diff --git a/src/views/tools/transform-note/text-to-speech/tts-job.tsx b/src/views/tools/transform-note/text-to-speech/tts-job.tsx index 0811eb7a0..c660b42fb 100644 --- a/src/views/tools/transform-note/text-to-speech/tts-job.tsx +++ b/src/views/tools/transform-note/text-to-speech/tts-job.tsx @@ -3,8 +3,8 @@ import codes from "iso-language-codes"; import { DVMJob, getRequestInputParam } from "../../../../helpers/nostr/dvm"; import { NostrEvent } from "../../../../types/nostr-event"; -import UserAvatarLink from "../../../../components/user-avatar-link"; -import UserLink from "../../../../components/user-link"; +import UserAvatarLink from "../../../../components/user/user-avatar-link"; +import UserLink from "../../../../components/user/user-link"; import Timestamp from "../../../../components/timestamp"; import { CodeIcon } from "../../../../components/icons"; import TextToSpeechResponse from "./tts-response"; diff --git a/src/views/tools/transform-note/text-to-speech/tts-result.tsx b/src/views/tools/transform-note/text-to-speech/tts-result.tsx index 5393cd52c..385688383 100644 --- a/src/views/tools/transform-note/text-to-speech/tts-result.tsx +++ b/src/views/tools/transform-note/text-to-speech/tts-result.tsx @@ -1,7 +1,7 @@ import { Flex, Text } from "@chakra-ui/react"; -import UserAvatarLink from "../../../../components/user-avatar-link"; -import UserLink from "../../../../components/user-link"; +import UserAvatarLink from "../../../../components/user/user-avatar-link"; +import UserLink from "../../../../components/user/user-link"; import { NostrEvent } from "../../../../types/nostr-event"; export default function TextToSpeechResult({ result }: { result: NostrEvent }) { diff --git a/src/views/tools/transform-note/text-to-speech/tts-status.tsx b/src/views/tools/transform-note/text-to-speech/tts-status.tsx index 1f0d764bf..915e2626d 100644 --- a/src/views/tools/transform-note/text-to-speech/tts-status.tsx +++ b/src/views/tools/transform-note/text-to-speech/tts-status.tsx @@ -2,8 +2,8 @@ import { MouseEventHandler, useState } from "react"; import { Button, Flex, Spacer, Text, useToast } from "@chakra-ui/react"; import { NostrEvent } from "../../../../types/nostr-event"; -import UserAvatarLink from "../../../../components/user-avatar-link"; -import UserLink from "../../../../components/user-link"; +import UserAvatarLink from "../../../../components/user/user-avatar-link"; +import UserLink from "../../../../components/user/user-link"; import { LightningIcon } from "../../../../components/icons"; import { readablizeSats } from "../../../../helpers/bolt11"; diff --git a/src/views/tools/transform-note/translation/index.tsx b/src/views/tools/transform-note/translation/index.tsx index 97cd712e3..e38e7773b 100644 --- a/src/views/tools/transform-note/translation/index.tsx +++ b/src/views/tools/transform-note/translation/index.tsx @@ -16,7 +16,7 @@ import codes from "iso-language-codes"; import { DraftNostrEvent, NostrEvent } from "../../../../types/nostr-event"; import useTimelineLoader from "../../../../hooks/use-timeline-loader"; -import { getEventUID } from "../../../../helpers/nostr/events"; +import { getEventUID } from "../../../../helpers/nostr/event"; import { useReadRelays } from "../../../../hooks/use-client-relays"; import useSubject from "../../../../hooks/use-subject"; import relayScoreboardService from "../../../../services/relay-scoreboard"; diff --git a/src/views/tools/transform-note/translation/translation-job.tsx b/src/views/tools/transform-note/translation/translation-job.tsx index 5486f08ef..4b4912c54 100644 --- a/src/views/tools/transform-note/translation/translation-job.tsx +++ b/src/views/tools/transform-note/translation/translation-job.tsx @@ -4,8 +4,8 @@ import codes from "iso-language-codes"; import { DVMJob, getRequestInputParam } from "../../../../helpers/nostr/dvm"; import { NostrEvent } from "../../../../types/nostr-event"; import Timestamp from "../../../../components/timestamp"; -import UserLink from "../../../../components/user-link"; -import UserAvatarLink from "../../../../components/user-avatar-link"; +import UserLink from "../../../../components/user/user-link"; +import UserAvatarLink from "../../../../components/user/user-avatar-link"; import TranslationResponse from "./translation-response"; import DebugEventButton from "../../../../components/debug-modal/debug-event-button"; diff --git a/src/views/tools/transform-note/translation/translation-result.tsx b/src/views/tools/transform-note/translation/translation-result.tsx index 15cccbdd8..3fcc6c8c2 100644 --- a/src/views/tools/transform-note/translation/translation-result.tsx +++ b/src/views/tools/transform-note/translation/translation-result.tsx @@ -1,9 +1,9 @@ import { Button, Flex, Text, useDisclosure } from "@chakra-ui/react"; -import UserAvatarLink from "../../../../components/user-avatar-link"; -import UserLink from "../../../../components/user-link"; -import { NoteContents } from "../../../../components/note/text-note-contents"; +import UserAvatarLink from "../../../../components/user/user-avatar-link"; +import UserLink from "../../../../components/user/user-link"; import { NostrEvent } from "../../../../types/nostr-event"; +import TextNoteContents from "../../../../components/note/timeline-note/text-note-contents"; export default function TranslationResult({ result }: { result: NostrEvent }) { const content = useDisclosure(); @@ -18,7 +18,7 @@ export default function TranslationResult({ result }: { result: NostrEvent }) { {content.isOpen ? "Hide" : "Show"} Content - {content.isOpen && } + {content.isOpen && } ); } diff --git a/src/views/tools/transform-note/translation/translation-status.tsx b/src/views/tools/transform-note/translation/translation-status.tsx index bf4a07822..4736c8616 100644 --- a/src/views/tools/transform-note/translation/translation-status.tsx +++ b/src/views/tools/transform-note/translation/translation-status.tsx @@ -2,8 +2,8 @@ import { MouseEventHandler, useState } from "react"; import { Button, Flex, Spacer, Text, useToast } from "@chakra-ui/react"; import { NostrEvent } from "../../../../types/nostr-event"; -import UserAvatarLink from "../../../../components/user-avatar-link"; -import UserLink from "../../../../components/user-link"; +import UserAvatarLink from "../../../../components/user/user-avatar-link"; +import UserLink from "../../../../components/user/user-link"; import { LightningIcon } from "../../../../components/icons"; import { readablizeSats } from "../../../../helpers/bolt11"; diff --git a/src/views/tools/unknown-event-feed.tsx b/src/views/tools/unknown-event-feed.tsx index 142a0b6fc..4e16a2c4b 100644 --- a/src/views/tools/unknown-event-feed.tsx +++ b/src/views/tools/unknown-event-feed.tsx @@ -18,7 +18,7 @@ import useSubject from "../../hooks/use-subject"; import { NostrEvent } from "../../types/nostr-event"; import { ChevronLeftIcon } from "../../components/icons"; import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { EmbedEvent } from "../../components/embed-event"; import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream"; import { diff --git a/src/views/tools/wot-test.tsx b/src/views/tools/wot-test.tsx index 3c2152ff2..d18689a29 100644 --- a/src/views/tools/wot-test.tsx +++ b/src/views/tools/wot-test.tsx @@ -4,8 +4,8 @@ import { Button, Flex, Select, SimpleGrid, Text } from "@chakra-ui/react"; import useCurrentAccount from "../../hooks/use-current-account"; import RequireCurrentAccount from "../../providers/route/require-current-account"; import { useNetworkConnectionCount } from "../../hooks/use-user-network"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import { ChevronLeftIcon } from "../../components/icons"; import { useNavigate } from "react-router-dom"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/torrents/components/torrent-comment-menu.tsx b/src/views/torrents/components/torrent-comment-menu.tsx index b6b3410ce..2da6cb7e1 100644 --- a/src/views/torrents/components/torrent-comment-menu.tsx +++ b/src/views/torrents/components/torrent-comment-menu.tsx @@ -1,4 +1,4 @@ -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; @@ -14,14 +14,14 @@ export default function TorrentCommentMenu({ }: { comment: NostrEvent; detailsClick?: () => void } & Omit) { return ( <> - + - + ); } diff --git a/src/views/torrents/components/torrent-menu.tsx b/src/views/torrents/components/torrent-menu.tsx index 309237bd5..8996388c0 100644 --- a/src/views/torrents/components/torrent-menu.tsx +++ b/src/views/torrents/components/torrent-menu.tsx @@ -1,6 +1,6 @@ import { MenuItem, useDisclosure } from "@chakra-ui/react"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { NostrEvent } from "../../../types/nostr-event"; import { TranslateIcon } from "../../../components/icons"; import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event"; @@ -18,7 +18,7 @@ export default function TorrentMenu({ return ( <> - + @@ -27,7 +27,7 @@ export default function TorrentMenu({ Translations - + {translationsModal.isOpen && } diff --git a/src/views/torrents/components/torrent-table-row.tsx b/src/views/torrents/components/torrent-table-row.tsx index 908373483..8b64f19d9 100644 --- a/src/views/torrents/components/torrent-table-row.tsx +++ b/src/views/torrents/components/torrent-table-row.tsx @@ -5,14 +5,14 @@ import { Link as RouterLink, useLocation } from "react-router-dom"; import { getTorrentMagnetLink, getTorrentSize, getTorrentTitle } from "../../../helpers/nostr/torrents"; import { NostrEvent } from "../../../types/nostr-event"; import Timestamp from "../../../components/timestamp"; -import UserLink from "../../../components/user-link"; +import UserLink from "../../../components/user/user-link"; import Magnet from "../../../components/icons/magnet"; import { getSharableEventAddress } from "../../../helpers/nip19"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { formatBytes } from "../../../helpers/number"; -import NoteZapButton from "../../../components/note/note-zap-button"; import TorrentMenu from "./torrent-menu"; +import NoteZapButton from "../../../components/note/note-zap-button"; type DisplayCategory = { name: string; tags: string[] }; @@ -57,8 +57,8 @@ function TorrentTableRow({ torrent }: { torrent: NostrEvent }) { ))} - - + + {getTorrentTitle(torrent)} diff --git a/src/views/torrents/components/torrents-comments.tsx b/src/views/torrents/components/torrents-comments.tsx index 8e949e475..bda1f0fec 100644 --- a/src/views/torrents/components/torrents-comments.tsx +++ b/src/views/torrents/components/torrents-comments.tsx @@ -22,21 +22,21 @@ import { useDisclosure, } from "@chakra-ui/react"; import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import Timestamp from "../../../components/timestamp"; import Minus from "../../../components/icons/minus"; import Expand01 from "../../../components/icons/expand-01"; import { TrustProvider } from "../../../providers/local/trust"; -import { NoteContents } from "../../../components/note/text-note-contents"; -import NoteReactions from "../../../components/note/components/note-reactions"; import { ReplyIcon } from "../../../components/icons"; import ReplyForm from "../../thread/components/reply-form"; import EventInteractionDetailsModal from "../../../components/event-interactions-modal"; -import NoteZapButton from "../../../components/note/note-zap-button"; import useThreadColorLevelProps from "../../../hooks/use-thread-color-level-props"; import TorrentCommentMenu from "./torrent-comment-menu"; +import NoteReactions from "../../../components/note/timeline-note/components/note-reactions"; +import NoteZapButton from "../../../components/note/note-zap-button"; +import { TextNoteContents } from "../../../components/note/timeline-note/text-note-contents"; export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level?: number }) => { const { showReactions } = useAppSettings(); @@ -91,7 +91,7 @@ export const ThreadPost = memo(({ post, level = -1 }: { post: ThreadItem; level? ) : ( <> - + ); diff --git a/src/views/torrents/index.tsx b/src/views/torrents/index.tsx index 9727e7e47..9e64b0ac6 100644 --- a/src/views/torrents/index.tsx +++ b/src/views/torrents/index.tsx @@ -16,7 +16,7 @@ import TorrentTableRow from "./components/torrent-table-row"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import useCurrentAccount from "../../hooks/use-current-account"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; import accountService from "../../services/account"; import signingService from "../../services/signing"; import CategorySelect from "./components/category-select"; diff --git a/src/views/torrents/torrent.tsx b/src/views/torrents/torrent.tsx index 081ce2ae4..d4c0963d6 100644 --- a/src/views/torrents/torrent.tsx +++ b/src/views/torrents/torrent.tsx @@ -23,8 +23,8 @@ import useSingleEvent from "../../hooks/use-single-event"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { NostrEvent } from "../../types/nostr-event"; import { ErrorBoundary } from "../../components/error-boundary"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import { TORRENT_COMMENT_KIND, getTorrentFiles, @@ -34,16 +34,16 @@ import { } from "../../helpers/nostr/torrents"; import Magnet from "../../components/icons/magnet"; import { formatBytes } from "../../helpers/number"; -import { NoteContents } from "../../components/note/text-note-contents"; import Timestamp from "../../components/timestamp"; -import NoteZapButton from "../../components/note/note-zap-button"; import TorrentMenu from "./components/torrent-menu"; -import QuoteRepostButton from "../../components/note/components/quote-repost-button"; import TorrentComments from "./components/torrents-comments"; import ReplyForm from "../thread/components/reply-form"; -import { getThreadReferences } from "../../helpers/nostr/events"; +import { getThreadReferences } from "../../helpers/nostr/event"; import MessageTextCircle01 from "../../components/icons/message-text-circle-01"; import useParamsEventPointer from "../../hooks/use-params-event-pointer"; +import NoteZapButton from "../../components/note/note-zap-button"; +import QuoteRepostButton from "../../components/note/quote-repost-button"; +import { TextNoteContents } from "../../components/note/timeline-note/text-note-contents"; function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) { const files = getTorrentFiles(torrent); @@ -87,7 +87,7 @@ function TorrentDetailsPage({ torrent }: { torrent: NostrEvent }) { Description - + )} diff --git a/src/views/tracks/components/track-card.tsx b/src/views/tracks/components/track-card.tsx index 4a5a9c359..f968d1fbb 100644 --- a/src/views/tracks/components/track-card.tsx +++ b/src/views/tracks/components/track-card.tsx @@ -1,22 +1,22 @@ import { useRef } from "react"; import { Button, ButtonGroup, Card, CardBody, CardFooter, CardHeader, CardProps, Flex, Tag } from "@chakra-ui/react"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import { NostrEvent } from "../../../types/nostr-event"; import { getHashtags } from "../../../helpers/nostr/stemstr"; import { CompactNoteContent } from "../../../components/compact-note-content"; import Timestamp from "../../../components/timestamp"; -import UserLink from "../../../components/user-link"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; import { ReplyIcon } from "../../../components/icons"; -import QuoteRepostButton from "../../../components/note/components/quote-repost-button"; -import NoteZapButton from "../../../components/note/note-zap-button"; import TrackStemstrButton from "./track-stemstr-button"; import TrackDownloadButton from "./track-download-button"; import TrackPlayer from "./track-player"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; import TrackMenu from "./track-menu"; +import QuoteRepostButton from "../../../components/note/quote-repost-button"; +import NoteZapButton from "../../../components/note/note-zap-button"; export default function TrackCard({ track, ...props }: { track: NostrEvent } & Omit) { const hashtags = getHashtags(track); diff --git a/src/views/tracks/components/track-menu.tsx b/src/views/tracks/components/track-menu.tsx index 9f83262fa..e57265b07 100644 --- a/src/views/tracks/components/track-menu.tsx +++ b/src/views/tracks/components/track-menu.tsx @@ -1,5 +1,5 @@ import { NostrEvent } from "../../../types/nostr-event"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; @@ -13,14 +13,14 @@ export default function TrackMenu({ }: { track: NostrEvent; detailsClick?: () => void } & Omit) { return ( <> - + - + ); } diff --git a/src/views/user/about/index.tsx b/src/views/user/about/index.tsx index fc33740c9..e9d3562b5 100644 --- a/src/views/user/about/index.tsx +++ b/src/views/user/about/index.tsx @@ -2,13 +2,13 @@ import { useOutletContext, Link as RouterLink } from "react-router-dom"; import { Box, Button, Flex, Heading, IconButton, Image, Link, Text, useDisclosure } from "@chakra-ui/react"; import { nip19 } from "nostr-tools"; -import { getUserDisplayName } from "../../../helpers/user-metadata"; +import { getUserDisplayName } from "../../../helpers/nostr/user-metadata"; import { getLudEndpoint } from "../../../helpers/lnurl"; import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; -import { truncatedId } from "../../../helpers/nostr/events"; +import { truncatedId } from "../../../helpers/nostr/event"; import { parseAddress } from "../../../services/dns-identity"; import { useAdditionalRelayContext } from "../../../providers/local/additional-relay-context"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; import { embedNostrLinks, renderGenericUrl } from "../../../components/embed-types"; import { ChevronDownIcon, @@ -20,10 +20,10 @@ import { } from "../../../components/icons"; import { CopyIconButton } from "../../../components/copy-icon-button"; import { QrIconButton } from "../components/share-qr-button"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; -import UserAvatar from "../../../components/user-avatar"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; +import UserAvatar from "../../../components/user/user-avatar"; import { ChatIcon } from "@chakra-ui/icons"; -import { UserFollowButton } from "../../../components/user-follow-button"; +import { UserFollowButton } from "../../../components/user/user-follow-button"; import UserZapButton from "../components/user-zap-button"; import { UserProfileMenu } from "../components/user-profile-menu"; import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id"; @@ -152,7 +152,7 @@ export default function UserAboutTab() { {truncatedId(npub, 10)} - + )} diff --git a/src/views/user/about/user-profile-badges.tsx b/src/views/user/about/user-profile-badges.tsx index 46428eaf4..a1aecbf03 100644 --- a/src/views/user/about/user-profile-badges.tsx +++ b/src/views/user/about/user-profile-badges.tsx @@ -20,11 +20,11 @@ import { Link as RouterLink } from "react-router-dom"; import useUserProfileBadges from "../../../hooks/use-user-profile-badges"; import { getBadgeDescription, getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges"; -import { getEventCoordinate } from "../../../helpers/nostr/events"; +import { getEventCoordinate } from "../../../helpers/nostr/event"; import { NostrEvent } from "../../../types/nostr-event"; import { getSharableEventAddress } from "../../../helpers/nip19"; -import UserAvatarLink from "../../../components/user-avatar-link"; -import UserLink from "../../../components/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; import Timestamp from "../../../components/timestamp"; import { useState } from "react"; diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 362ddee55..daa4e5e46 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -2,13 +2,13 @@ import { Flex, Heading, IconButton, Spacer } from "@chakra-ui/react"; import { useNavigate } from "react-router-dom"; import { EditIcon, GhostIcon } from "../../../components/icons"; -import UserAvatar from "../../../components/user-avatar"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; -import { getUserDisplayName } from "../../../helpers/user-metadata"; +import UserAvatar from "../../../components/user/user-avatar"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; +import { getUserDisplayName } from "../../../helpers/nostr/user-metadata"; import useCurrentAccount from "../../../hooks/use-current-account"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; import { UserProfileMenu } from "./user-profile-menu"; -import { UserFollowButton } from "../../../components/user-follow-button"; +import { UserFollowButton } from "../../../components/user/user-follow-button"; import accountService from "../../../services/account"; import { useBreakpointValue } from "../../../providers/global/breakpoint-provider"; diff --git a/src/views/user/components/share-qr-button.tsx b/src/views/user/components/share-qr-button.tsx index 0841b15f1..dbaa83540 100644 --- a/src/views/user/components/share-qr-button.tsx +++ b/src/views/user/components/share-qr-button.tsx @@ -18,7 +18,7 @@ import { import { nip19 } from "nostr-tools"; import { QrCodeIcon } from "../../../components/icons"; -import QrCodeSvg from "../../../components/qr-code-svg"; +import QrCodeSvg from "../../../components/qr-code/qr-code-svg"; import { CopyIconButton } from "../../../components/copy-icon-button"; import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id"; @@ -36,7 +36,6 @@ export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit - @@ -49,19 +48,20 @@ export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit - + - + + diff --git a/src/views/user/components/user-card.tsx b/src/views/user/components/user-card.tsx index 363a8ad3d..44c769006 100644 --- a/src/views/user/components/user-card.tsx +++ b/src/views/user/components/user-card.tsx @@ -1,9 +1,9 @@ import { Flex, FlexProps } from "@chakra-ui/react"; -import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; -import { UserFollowButton } from "../../../components/user-follow-button"; -import UserLink from "../../../components/user-link"; -import UserAvatarLink from "../../../components/user-avatar-link"; +import { UserDnsIdentityIcon } from "../../../components/user/user-dns-identity-icon"; +import { UserFollowButton } from "../../../components/user/user-follow-button"; +import UserLink from "../../../components/user/user-link"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; export type UserCardProps = { pubkey: string; relay?: string } & Omit; diff --git a/src/views/user/components/user-profile-menu.tsx b/src/views/user/components/user-profile-menu.tsx index 745fb3c57..36af37d8f 100644 --- a/src/views/user/components/user-profile-menu.tsx +++ b/src/views/user/components/user-profile-menu.tsx @@ -1,8 +1,8 @@ -import { MenuItem, useDisclosure, useToast } from "@chakra-ui/react"; +import { MenuItem, useConst, useDisclosure, useToast } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { nip19 } from "nostr-tools"; -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import { DirectMessagesIcon, CopyToClipboardIcon, @@ -15,15 +15,16 @@ import { ShareIcon, } from "../../../components/icons"; import accountService from "../../../services/account"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; -import { getUserDisplayName } from "../../../helpers/user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../../helpers/nostr/user-metadata"; import UserDebugModal from "../../../components/debug-modal/user-debug-modal"; import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id"; -import { buildAppSelectUrl } from "../../../helpers/nostr/apps"; -import { truncatedId } from "../../../helpers/nostr/events"; +import { truncatedId } from "../../../helpers/nostr/event"; import useUserMuteActions from "../../../hooks/use-user-mute-actions"; import useCurrentAccount from "../../../hooks/use-current-account"; import userMailboxesService from "../../../services/user-mailboxes"; +import { useContext } from "react"; +import { AppHandlerContext } from "../../../providers/route/app-handler-provider"; export const UserProfileMenu = ({ pubkey, @@ -36,6 +37,7 @@ export const UserProfileMenu = ({ const infoModal = useDisclosure(); const sharableId = useSharableProfileId(pubkey); const { isMuted, mute, unmute } = useUserMuteActions(pubkey); + const { openAddress } = useContext(AppHandlerContext); const loginAsUser = () => { const relays = userMailboxesService.getMailboxes(pubkey).value?.outbox.urls; @@ -52,8 +54,8 @@ export const UserProfileMenu = ({ return ( <> - - window.open(buildAppSelectUrl(sharableId), "_blank")} icon={}> + + openAddress(sharableId)} icon={}> View in app... {account?.pubkey !== pubkey && ( @@ -95,7 +97,7 @@ export const UserProfileMenu = ({ Relay selection )} - + {infoModal.isOpen && ( )} diff --git a/src/views/user/components/user-zap-button.tsx b/src/views/user/components/user-zap-button.tsx index 178682b4d..af7608a5c 100644 --- a/src/views/user/components/user-zap-button.tsx +++ b/src/views/user/components/user-zap-button.tsx @@ -1,5 +1,5 @@ import { IconButton, IconButtonProps, useDisclosure } from "@chakra-ui/react"; -import { useUserMetadata } from "../../../hooks/use-user-metadata"; +import useUserMetadata from "../../../hooks/use-user-metadata"; import { LightningIcon } from "../../../components/icons"; import ZapModal from "../../../components/event-zap-modal"; import { useInvoiceModalContext } from "../../../providers/route/invoice-modal"; diff --git a/src/views/user/dms.tsx b/src/views/user/dms.tsx index 6ba242680..a77c10f22 100644 --- a/src/views/user/dms.tsx +++ b/src/views/user/dms.tsx @@ -13,13 +13,13 @@ import IntersectionObserverProvider, { import VerticalPageLayout from "../../components/vertical-page-layout"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import { NostrEvent, isPTag } from "../../types/nostr-event"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import ArrowRight from "../../components/icons/arrow-right"; import { AtIcon } from "../../components/icons"; import Timestamp from "../../components/timestamp"; import ArrowLeft from "../../components/icons/arrow-left"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; function DirectMessage({ dm, pubkey }: { dm: NostrEvent; pubkey: string }) { const sender = dm.pubkey; diff --git a/src/views/user/emoji-packs.tsx b/src/views/user/emoji-packs.tsx index a642db472..e55458676 100644 --- a/src/views/user/emoji-packs.tsx +++ b/src/views/user/emoji-packs.tsx @@ -4,7 +4,7 @@ import { Heading, SimpleGrid } from "@chakra-ui/react"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import EmojiPackCard from "../emoji-packs/components/emoji-pack-card"; diff --git a/src/views/user/followers.tsx b/src/views/user/followers.tsx index e2ef4c799..71a959437 100644 --- a/src/views/user/followers.tsx +++ b/src/views/user/followers.tsx @@ -11,9 +11,9 @@ import IntersectionObserverProvider, { } from "../../providers/local/intersection-observer"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import { useMemo, useRef } from "react"; -import { getEventUID } from "../../helpers/nostr/events"; -import UserLink from "../../components/user-link"; -import UserAvatarLink from "../../components/user-avatar-link"; +import { getEventUID } from "../../helpers/nostr/event"; +import UserLink from "../../components/user/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; function FollowerItem({ event }: { event: Event }) { const ref = useRef(null); diff --git a/src/views/user/goals.tsx b/src/views/user/goals.tsx index 79c426009..0e8148890 100644 --- a/src/views/user/goals.tsx +++ b/src/views/user/goals.tsx @@ -4,7 +4,7 @@ import { Flex, SimpleGrid } from "@chakra-ui/react"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { GOAL_KIND } from "../../helpers/nostr/goal"; diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index 6417c95b3..8e83a294c 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -28,8 +28,8 @@ import { import { kinds } from "nostr-tools"; import { Outlet, useMatches, useNavigate } from "react-router-dom"; -import { useUserMetadata } from "../../hooks/use-user-metadata"; -import { getUserDisplayName } from "../../helpers/user-metadata"; +import useUserMetadata from "../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../helpers/nostr/user-metadata"; import { useAppTitle } from "../../hooks/use-app-title"; import { useReadRelays } from "../../hooks/use-client-relays"; import relayScoreboardService from "../../services/relay-scoreboard"; diff --git a/src/views/user/lists.tsx b/src/views/user/lists.tsx index 5b46593da..e6363f0de 100644 --- a/src/views/user/lists.tsx +++ b/src/views/user/lists.tsx @@ -14,13 +14,13 @@ import { PIN_LIST_KIND, isJunkList, } from "../../helpers/nostr/lists"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import ListCard from "../lists/components/list-card"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { NostrEvent } from "../../types/nostr-event"; -import UserName from "../../components/user-name"; +import UserName from "../../components/user/user-name"; export default function UserListsTab() { const { pubkey } = useOutletContext() as { pubkey: string }; diff --git a/src/views/user/muted-by.tsx b/src/views/user/muted-by.tsx index bc3329503..655bf96bc 100644 --- a/src/views/user/muted-by.tsx +++ b/src/views/user/muted-by.tsx @@ -2,8 +2,8 @@ import { memo, useMemo, useRef } from "react"; import { Flex, Heading, Link, SimpleGrid } from "@chakra-ui/react"; import { Link as RouterLink, useOutletContext } from "react-router-dom"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { useReadRelays } from "../../hooks/use-client-relays"; import { MUTE_LIST_KIND, PEOPLE_LIST_KIND, getListName, getPubkeysFromList } from "../../helpers/nostr/lists"; @@ -12,7 +12,7 @@ import IntersectionObserverProvider, { useRegisterIntersectionEntity, } from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { NostrEvent } from "../../types/nostr-event"; import SuperMap from "../../classes/super-map"; diff --git a/src/views/user/notes.tsx b/src/views/user/notes.tsx index 098c86b35..f697b6f7a 100644 --- a/src/views/user/notes.tsx +++ b/src/views/user/notes.tsx @@ -3,7 +3,7 @@ import { Flex, Spacer } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; import { kinds } from "nostr-tools"; -import { isReply, isRepost, truncatedId } from "../../helpers/nostr/events"; +import { isReply, isRepost, truncatedId } from "../../helpers/nostr/event"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import { RelayIconStack } from "../../components/relay-icon-stack"; import { NostrEvent } from "../../types/nostr-event"; diff --git a/src/views/user/reactions.tsx b/src/views/user/reactions.tsx index a3a363f70..f5ff97267 100644 --- a/src/views/user/reactions.tsx +++ b/src/views/user/reactions.tsx @@ -14,12 +14,12 @@ import IntersectionObserverProvider, { } from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { TrustProvider } from "../../providers/local/trust"; -import UserAvatar from "../../components/user-avatar"; -import UserLink from "../../components/user-link"; -import NoteMenu from "../../components/note/note-menu"; +import UserAvatar from "../../components/user/user-avatar"; +import UserLink from "../../components/user/user-link"; import { EmbedEventPointer } from "../../components/embed-event"; import { embedEmoji } from "../../components/embed-types"; import VerticalPageLayout from "../../components/vertical-page-layout"; +import NoteMenu from "../../components/note/note-menu"; const Reaction = ({ reaction: reaction }: { reaction: NostrEvent }) => { const ref = useRef(null); diff --git a/src/views/user/reports.tsx b/src/views/user/reports.tsx index 283d10d56..4491e66de 100644 --- a/src/views/user/reports.tsx +++ b/src/views/user/reports.tsx @@ -3,9 +3,9 @@ import { Flex, Text } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; import { kinds } from "nostr-tools"; -import { NoteLink } from "../../components/note-link"; -import UserLink from "../../components/user-link"; -import { filterTagsByContentRefs, getEventUID } from "../../helpers/nostr/events"; +import { NoteLink } from "../../components/note/note-link"; +import UserLink from "../../components/user/user-link"; +import { filterTagsByContentRefs, getEventUID } from "../../helpers/nostr/event"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { isETag, isPTag, NostrEvent } from "../../types/nostr-event"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; diff --git a/src/views/user/streams.tsx b/src/views/user/streams.tsx index c9ad2af46..c6e59bb02 100644 --- a/src/views/user/streams.tsx +++ b/src/views/user/streams.tsx @@ -1,6 +1,6 @@ import { SimpleGrid } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; -import { truncatedId } from "../../helpers/nostr/events"; +import { truncatedId } from "../../helpers/nostr/event"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; diff --git a/src/views/user/tracks.tsx b/src/views/user/tracks.tsx index c418ee295..e81cd3332 100644 --- a/src/views/user/tracks.tsx +++ b/src/views/user/tracks.tsx @@ -5,7 +5,7 @@ import { Box, SimpleGrid } from "@chakra-ui/react"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import IntersectionObserverProvider, { useRegisterIntersectionEntity, } from "../../providers/local/intersection-observer"; diff --git a/src/views/user/videos.tsx b/src/views/user/videos.tsx index db4ccd9ce..7df10e6ee 100644 --- a/src/views/user/videos.tsx +++ b/src/views/user/videos.tsx @@ -4,7 +4,7 @@ import { SimpleGrid } from "@chakra-ui/react"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import VerticalPageLayout from "../../components/vertical-page-layout"; diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx index a3ee3680b..e09c96901 100644 --- a/src/views/user/zaps.tsx +++ b/src/views/user/zaps.tsx @@ -5,9 +5,9 @@ import { useOutletContext } from "react-router-dom"; import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary"; import { LightningIcon } from "../../components/icons"; -import { NoteLink } from "../../components/note-link"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; +import { NoteLink } from "../../components/note/note-link"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; import { readablizeSats } from "../../helpers/bolt11"; import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/nostr/zaps"; import useTimelineLoader from "../../hooks/use-timeline-loader"; @@ -24,7 +24,7 @@ import { EmbedableContent, embedUrls } from "../../helpers/embeds"; import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types"; import Timestamp from "../../components/timestamp"; import { EmbedEventNostrLink, EmbedEventPointer } from "../../components/embed-event"; -import { parseCoordinate } from "../../helpers/nostr/events"; +import { parseCoordinate } from "../../helpers/nostr/event"; import VerticalPageLayout from "../../components/vertical-page-layout"; const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => { diff --git a/src/views/videos/components/video-card.tsx b/src/views/videos/components/video-card.tsx index 42e3c5908..1da261e37 100644 --- a/src/views/videos/components/video-card.tsx +++ b/src/views/videos/components/video-card.tsx @@ -5,7 +5,7 @@ import { Link as RouterLink } from "react-router-dom"; import { NostrEvent } from "../../../types/nostr-event"; import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer"; import { getVideoDuration, getVideoImages, getVideoSummary, getVideoTitle } from "../../../helpers/nostr/flare"; -import { getEventUID } from "../../../helpers/nostr/events"; +import { getEventUID } from "../../../helpers/nostr/event"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; import { getSharableEventAddress } from "../../../helpers/nip19"; diff --git a/src/views/videos/components/video-menu.tsx b/src/views/videos/components/video-menu.tsx index 41e11065b..6d5920164 100644 --- a/src/views/videos/components/video-menu.tsx +++ b/src/views/videos/components/video-menu.tsx @@ -1,4 +1,4 @@ -import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button"; +import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button"; import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app"; import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link"; import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code"; @@ -9,13 +9,13 @@ import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu export default function VideoMenu({ video, ...props }: { video: NostrEvent } & Omit) { return ( <> - + - + ); } diff --git a/src/views/videos/index.tsx b/src/views/videos/index.tsx index 027672746..f6e5e0933 100644 --- a/src/views/videos/index.tsx +++ b/src/views/videos/index.tsx @@ -12,7 +12,7 @@ import Upload01 from "../../components/icons/upload-01"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare"; import VideoCard from "./components/video-card"; -import { getEventUID } from "../../helpers/nostr/events"; +import { getEventUID } from "../../helpers/nostr/event"; import { ErrorBoundary } from "../../components/error-boundary"; import { useReadRelays } from "../../hooks/use-client-relays"; diff --git a/src/views/videos/video.tsx b/src/views/videos/video.tsx index b97c609ef..0704e48c0 100644 --- a/src/views/videos/video.tsx +++ b/src/views/videos/video.tsx @@ -11,24 +11,24 @@ import { import { NostrEvent } from "../../types/nostr-event"; import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; -import UserAvatarLink from "../../components/user-avatar-link"; -import UserLink from "../../components/user-link"; -import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon"; -import { UserFollowButton } from "../../components/user-follow-button"; +import UserAvatarLink from "../../components/user/user-avatar-link"; +import UserLink from "../../components/user/user-link"; +import { UserDnsIdentityIcon } from "../../components/user/user-dns-identity-icon"; +import { UserFollowButton } from "../../components/user/user-follow-button"; import VideoMenu from "./components/video-menu"; -import NoteZapButton from "../../components/note/note-zap-button"; import SimpleLikeButton from "../../components/event-reactions/simple-like-button"; import SimpleDislikeButton from "../../components/event-reactions/simple-dislike-button"; import { ErrorBoundary } from "../../components/error-boundary"; -import QuoteRepostButton from "../../components/note/components/quote-repost-button"; import { useReadRelays } from "../../hooks/use-client-relays"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import useSubject from "../../hooks/use-subject"; import VideoCard from "./components/video-card"; -import { getEventUID } from "../../helpers/nostr/events"; -import UserName from "../../components/user-name"; +import { getEventUID } from "../../helpers/nostr/event"; +import UserName from "../../components/user/user-name"; import { useBreakpointValue } from "../../providers/global/breakpoint-provider"; import SimpleBookmarkButton from "../../components/simple-bookmark-button"; +import NoteZapButton from "../../components/note/note-zap-button"; +import QuoteRepostButton from "../../components/note/quote-repost-button"; function VideoRecommendations({ video }: { video: NostrEvent }) { const readRelays = useReadRelays(); diff --git a/yarn.lock b/yarn.lock index 7737ddf25..477e7085c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -935,7 +935,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.1", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.1", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== @@ -1843,6 +1843,15 @@ dependencies: "@changesets/types" "^6.0.0" +"@changesets/changelog-github@^0.4.8": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@changesets/changelog-github/-/changelog-github-0.4.8.tgz#b7f8ae85d0c0ff08028d924c5e59a1cbd3742634" + integrity sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw== + dependencies: + "@changesets/get-github-info" "^0.5.2" + "@changesets/types" "^5.2.1" + dotenv "^8.1.0" + "@changesets/cli@^2.27.1": version "2.27.1" resolved "https://registry.yarnpkg.com/@changesets/cli/-/cli-2.27.1.tgz#abce480fd30b9abbe2cfcf07d5d668c364ce2804" @@ -1912,6 +1921,14 @@ fs-extra "^7.0.1" semver "^7.5.3" +"@changesets/get-github-info@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@changesets/get-github-info/-/get-github-info-0.5.2.tgz#0cde2cadba57db85c714dc303c077da919a574e5" + integrity sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg== + dependencies: + dataloader "^1.4.0" + node-fetch "^2.5.0" + "@changesets/get-release-plan@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@changesets/get-release-plan/-/get-release-plan-4.0.0.tgz#8cb057da90a08796a335dfd18073234d33902069" @@ -1988,6 +2005,11 @@ resolved "https://registry.yarnpkg.com/@changesets/types/-/types-4.1.0.tgz#fb8f7ca2324fd54954824e864f9a61a82cb78fe0" integrity sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw== +"@changesets/types@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@changesets/types/-/types-5.2.1.tgz#a228c48004aa8a93bce4be2d1d31527ef3bf21f6" + integrity sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg== + "@changesets/types@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@changesets/types/-/types-6.0.0.tgz#e46abda9890610dd1fbe1617730173d2267544bd" @@ -2004,6 +2026,88 @@ human-id "^1.0.2" prettier "^2.7.1" +"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz#3fa620a8a3f42ded7751749916e8375f6bbbb333" + integrity sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.17.0" + "@lezer/common" "^1.0.0" + +"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0": + version "6.3.3" + resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.3.3.tgz#03face5bf5f3de0fc4e09b177b3c91eda2ceb7e9" + integrity sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.4.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.1.0" + +"@codemirror/lang-json@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz#0a0be701a5619c4b0f8991f9b5e95fe33f462330" + integrity sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/json" "^1.0.0" + +"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.1": + version "6.10.1" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.1.tgz#428c932a158cb75942387acfe513c1ece1090b05" + integrity sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.23.0" + "@lezer/common" "^1.1.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + style-mod "^4.0.0" + +"@codemirror/lint@^6.0.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.5.0.tgz#ea43b6e653dcc5bcd93456b55e9fe62e63f326d9" + integrity sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/search@^6.0.0": + version "6.5.6" + resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.6.tgz#8f858b9e678d675869112e475f082d1e8488db93" + integrity sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.0.tgz#8bc3e096c84360b34525a84696a84f86b305363a" + integrity sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A== + +"@codemirror/theme-one-dark@^6.0.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8" + integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/highlight" "^1.0.0" + +"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0": + version "6.24.0" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.24.0.tgz#2f780290a54cfe571b1a1468c47b483de0cf6fb2" + integrity sha512-zK6m5pNkdhdJl8idPP1gA4N8JKTiSsOz8U/Iw+C1ChMwyLG7+MLiNXnH/wFuAk6KeGEe33/adOiAh5jMqee03w== + dependencies: + "@codemirror/state" "^6.4.0" + style-mod "^4.1.0" + w3c-keyname "^2.2.4" + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -2323,6 +2427,34 @@ resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== +"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049" + integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ== + +"@lezer/highlight@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780" + integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA== + dependencies: + "@lezer/common" "^1.0.0" + +"@lezer/json@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.2.tgz#bdc849e174113e2d9a569a5e6fb1a27e2f703eaf" + integrity sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ== + dependencies: + "@lezer/common" "^1.2.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/lr@^1.0.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.0.tgz#ed52a75dbbfbb0d1eb63710ea84c35ee647cb67e" + integrity sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg== + dependencies: + "@lezer/common" "^1.0.0" + "@lightninglabs/lnc-core@0.2.8-alpha": version "0.2.8-alpha" resolved "https://registry.yarnpkg.com/@lightninglabs/lnc-core/-/lnc-core-0.2.8-alpha.tgz#78272c04a5ec95a9ccb830f75ab9b5ca227f0801" @@ -2552,6 +2684,19 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz#2c1fb69e02a3f1506f52698cfdc3a8b6386df9a6" integrity sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ== +"@sagold/json-pointer@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@sagold/json-pointer/-/json-pointer-5.1.1.tgz#01c2b75a3ea09eebde8c83a8c11409c8573dea67" + integrity sha512-/iskWuyGNu09qy09HYmyLnvzpKryymH9T+vTBi2LdFp1TuKvERDADvPMv2ZkQKsrRklOzivmOz9QXof0dKqvgA== + +"@sagold/json-query@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@sagold/json-query/-/json-query-6.1.1.tgz#d822c08c9d0760eadd12f9587a4da0b51c309152" + integrity sha512-5/Wu0rTnXmO5Uvtm9Of16Vx3mKjSnYA0Um9LgBtyPhIucYlppKgKC4N3g8gD0Fk00a5kizQTs4gwxKPXCpmeww== + dependencies: + "@sagold/json-pointer" "^5.1.1" + ebnf "^1.9.1" + "@scure/base@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" @@ -2710,6 +2855,11 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3" integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/leaflet.locatecontrol@^0.74.4": version "0.74.4" resolved "https://registry.yarnpkg.com/@types/leaflet.locatecontrol/-/leaflet.locatecontrol-0.74.4.tgz#15cb1f3a5c49956da9dadeea7e65b0e53193ff09" @@ -2770,6 +2920,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@^20.4.2": + version "20.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.17.tgz#cdd642d0e62ef3a861f88ddbc2b61e32578a9292" + integrity sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -2855,6 +3012,52 @@ resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.13.tgz#5fd07863819c30869d66b765926d0b5a53a7e9e0" integrity sha512-Hi4K3aTEoaa31Cep75AA9wK5q2iZgC1L70serPbI11L4YieoZpu5LvLr6FZXyIdqkkGPh1WMuDf6oSPHJXBkoA== +"@types/zen-observable@^0.8.7": + version "0.8.7" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.7.tgz#114e2ffc8d5be4915fdd5bc90668fc0ceaadb760" + integrity sha512-LKzNTjj+2j09wAo/vvVjzgw5qckJJzhdGgWHW7j69QIGdq/KnZrMAMIHQiWGl3Ccflh5/CudBAntTPYdprPltA== + +"@uiw/codemirror-extensions-basic-setup@4.21.21": + version "4.21.21" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.21.tgz#243ef309cb53253b14187649a7abc0d996420a20" + integrity sha512-+0i9dPrRSa8Mf0CvyrMvnAhajnqwsP3IMRRlaHDRgsSGL8igc4z7MhvUPn+7cWFAAqWzQRhMdMSWzo6/TEa3EA== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/commands" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/search" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + +"@uiw/codemirror-theme-github@^4.21.21": + version "4.21.21" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.21.21.tgz#0e0fd13500e6c17032a9b9fccb276e13f6681a91" + integrity sha512-msrpNrKk/CZQHk58TshI8aH7FpEyL404m/vWlGUdL2jGW7IRKm0nXn1lXXQ/snzk65h88GO6u9fiiv0pxRuZfQ== + dependencies: + "@uiw/codemirror-themes" "4.21.21" + +"@uiw/codemirror-themes@4.21.21": + version "4.21.21" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.21.21.tgz#26efb06ecce9a51aa73d39311c90f8fcb06fdc43" + integrity sha512-ljVcMGdaxo75UaH+EqxJ+jLyMVVgeSfW2AKyT1VeLy+4SDpuqNQ7wq5XVxktsG6LH+OvgSFndWXgPANf4+gQcA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + +"@uiw/react-codemirror@^4.21.21": + version "4.21.21" + resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.21.21.tgz#986b18dbd6dc69aa470fc3d4e47b89b504af6778" + integrity sha512-PaxBMarufMWoR0qc5zuvBSt76rJ9POm9qoOaJbqRmnNL2viaF+d+Paf2blPSlm1JSnqn7hlRjio+40nZJ9TKzw== + dependencies: + "@babel/runtime" "^7.18.6" + "@codemirror/commands" "^6.1.0" + "@codemirror/state" "^6.1.1" + "@codemirror/theme-one-dark" "^6.0.0" + "@uiw/codemirror-extensions-basic-setup" "4.21.21" + codemirror "^6.0.0" + "@vitejs/plugin-react@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz#744d8e4fcb120fc3dbaa471dadd3483f5a304bb9" @@ -3300,6 +3503,48 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +codemirror-json-schema@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/codemirror-json-schema/-/codemirror-json-schema-0.6.1.tgz#9f9088d2c747dd38934c4f0088e14b5c50260837" + integrity sha512-QG12Jy917eStZzxurpAE9QUQxF8SS/AYJ9DDteyJZcRGH8ePaBCfQ4KLCNtY6cUKjFeNBgcd5+c6FPAri6pPQg== + dependencies: + "@changesets/changelog-github" "^0.4.8" + "@sagold/json-pointer" "^5.1.1" + "@types/json-schema" "^7.0.12" + "@types/node" "^20.4.2" + json-schema "^0.4.0" + json-schema-library "^9.1.2" + optionalDependencies: + "@codemirror/lang-json" "^6.0.1" + codemirror-json5 "^1.0.3" + json5 "^2.2.3" + +codemirror-json5@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/codemirror-json5/-/codemirror-json5-1.0.3.tgz#046101609776a97ae3bd0bfc4b9f15638e317793" + integrity sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/common" "^1.0.0" + "@lezer/highlight" "^1.0.0" + json5 "^2.2.1" + lezer-json5 "^2.0.2" + +codemirror@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" + integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/commands" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/search" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -3329,7 +3574,7 @@ color2k@^2.0.2: resolved "https://registry.yarnpkg.com/color2k/-/color2k-2.0.3.tgz#a771244f6b6285541c82aa65ff0a0c624046e533" integrity sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog== -commander@^2.20.0: +commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3384,6 +3629,11 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +crelt@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" + integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== + cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -3616,6 +3866,11 @@ data-joint@1: dependencies: index-array-by "^1.4.0" +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + dayjs@^1.11.9: version "1.11.10" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" @@ -3641,7 +3896,7 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -deepmerge@^4.2.2: +deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -3689,6 +3944,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== + dnd-core@^16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" @@ -3733,11 +3993,21 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" +dotenv@^8.1.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" + integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== + draco3d@^1.4.1: version "1.5.7" resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.5.7.tgz#94f9bce293eb8920c159dc91a4ce9124a9e899e0" integrity sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ== +ebnf@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ebnf/-/ebnf-1.9.1.tgz#64c25d8208ec0d221ec11c3c5e8094015131a9d3" + integrity sha512-uW2UKSsuty9ANJ3YByIQE4ANkD8nqUPO7r6Fwcc1ADKPe9FRdcPpMl3VEput4JSvKBJ4J86npIC2MLP0pYkCuw== + ejs@^3.1.6: version "3.1.9" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" @@ -3755,10 +4025,10 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojilib@2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" - integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== +emojilib@^3: + version "3.0.11" + resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-3.0.11.tgz#10ba04e72e52e91b960d62f80f8463e2405a1b46" + integrity sha512-OuML9z640prB+0Rms1RgzJrBAqNC9mFdqT2HeFcXSGSGNEiZh9adsaPPxWbqRoMyJPzu+zNKPyGKv4/sPmJDyg== enquirer@^2.3.0: version "2.4.1" @@ -3940,6 +4210,11 @@ external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-copy@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" + integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4661,6 +4936,19 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-library@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/json-schema-library/-/json-schema-library-9.1.2.tgz#2cd276717d1247294dbd38b45ee577d4d88ba741" + integrity sha512-uQnFb2V+VakLl6XIGGtUQzfjkP31f/dCT5lJq9NOUdypSSpjbWL/V0R2KvoNJp3hU8VErwh9DqVoZPqlC+B3IA== + dependencies: + "@sagold/json-pointer" "^5.1.1" + "@sagold/json-query" "^6.1.1" + deepmerge "^4.3.1" + fast-copy "^3.0.1" + fast-deep-equal "^3.1.3" + smtp-address-parser "1.0.10" + valid-url "^1.0.9" + json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" @@ -4676,7 +4964,7 @@ json-stringify-deterministic@^1.0.12: resolved "https://registry.yarnpkg.com/json-stringify-deterministic/-/json-stringify-deterministic-1.0.12.tgz#aaa3f907466ed01e3afd77b898d0a2b3b132820a" integrity sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g== -json5@^2.2.0, json5@^2.2.3: +json5@^2.2.0, json5@^2.2.1, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -4734,6 +5022,13 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +lezer-json5@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lezer-json5/-/lezer-json5-2.0.2.tgz#ba3756e0d352d9529517dcf264d27db8579ae577" + integrity sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ== + dependencies: + "@lezer/lr" "^1.0.0" + light-bolt11-decoder@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz#f644576120426c9ef65621bde254f11016055044" @@ -4937,6 +5232,11 @@ mixme@^0.5.1: resolved "https://registry.yarnpkg.com/mixme/-/mixme-0.5.10.tgz#d653b2984b75d9018828f1ea333e51717ead5f51" integrity sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q== +moo@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4966,6 +5266,16 @@ nanoid@^5.0.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.5.tgz#5112efb5c0caf4fc80680d66d303c65233a79fdd" integrity sha512-/Veqm+QKsyMY3kqi4faWplnY1u+VuKO3dD2binyPIybP31DRO29bPF+1mszgLnrR2KqSLceFLBNw0zmvDzN1QQ== +nearley@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + ngeohash@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/ngeohash/-/ngeohash-0.6.3.tgz#10b1e80be5488262ec95c56cf2dbb6c45fbdf245" @@ -5002,6 +5312,13 @@ ngraph.random@^1.0.0: resolved "https://registry.yarnpkg.com/ngraph.random/-/ngraph.random-1.1.0.tgz#5345c4bb63865c85d98ee6f13eab1395d8545a90" integrity sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw== +node-fetch@^2.5.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-releases@^2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" @@ -5310,6 +5627,19 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5710,6 +6040,11 @@ resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -5906,6 +6241,13 @@ smartwrap@^2.0.2: wcwidth "^1.0.1" yargs "^15.1.0" +smtp-address-parser@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz#9fc4ed6021f13dc3d8f591e0ad0d50454073025e" + integrity sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg== + dependencies: + nearley "^2.20.1" + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -6105,6 +6447,11 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +style-mod@^4.0.0, style-mod@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.0.tgz#a313a14f4ae8bb4d52878c0053c4327fb787ec09" + integrity sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA== + stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" @@ -6268,6 +6615,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -6470,6 +6822,11 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +valid-url@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" + integrity sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -6500,6 +6857,11 @@ vite@^5.0.10: optionalDependencies: fsevents "~2.3.3" +w3c-keyname@^2.2.4: + version "2.2.8" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" + integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -6507,6 +6869,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -6519,6 +6886,14 @@ webln@^0.3.2: dependencies: "@types/chrome" "^0.0.74" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -6834,6 +7209,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zen-observable@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.10.0.tgz#ee10eba75272897dbee5f152ab26bb5e0107f0c8" + integrity sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw== + zustand@^4.4.7: version "4.5.0" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.0.tgz#141354af56f91de378aa6c4b930032ab338f3ef0"