diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index d811ddc38..9c3d0e67f 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -6,7 +6,7 @@ if [ -n "$CACHE_RELAY" ]; then echo "Cache relay set to $CACHE_RELAY" sed -i 's/CACHE_RELAY_ENABLED = false/CACHE_RELAY_ENABLED = true/g' /usr/share/nginx/html/index.html CACHE_RELAY_PROXY=" - location /cache-relay { + location /local-relay { proxy_pass http://$CACHE_RELAY/; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts index bfeae84a7..0685916f3 100644 --- a/src/classes/timeline-loader.ts +++ b/src/classes/timeline-loader.ts @@ -19,7 +19,7 @@ import { mapQueryMap, stringifyFilter, } from "../helpers/nostr/filter"; -import { localCacheRelay } from "../services/local-cache-relay"; +import { localRelay } from "../services/local-relay"; import { relayRequest } from "../helpers/relay"; import { Subscription } from "nostr-idb"; @@ -147,7 +147,7 @@ export default class TimelineLoader { if (isReplaceable(event.kind)) replaceableEventLoaderService.handleEvent(event); this.events.addEvent(event); - if (cache) localCacheRelay.publish(event); + if (cache) localRelay.publish(event); } private handleDeleteEvent(deleteEvent: NostrEvent) { const cord = deleteEvent.tags.find(isATag)?.[1]; @@ -177,7 +177,7 @@ export default class TimelineLoader { } for (const filters of Object.values(queries)) { - relayRequest(localCacheRelay, filters).then((events) => { + relayRequest(localRelay, filters).then((events) => { for (const e of events) this.handleEvent(e, false); }); } diff --git a/src/components/embed-types/common.tsx b/src/components/embed-types/common.tsx index 7b595c98d..fc2c4380c 100644 --- a/src/components/embed-types/common.tsx +++ b/src/components/embed-types/common.tsx @@ -6,7 +6,12 @@ import OpenGraphLink from "../open-graph-link"; export function renderGenericUrl(match: URL) { return ( - {match.toString()} + {match.protocol + + "//" + + match.host + + match.pathname + + (match.search && match.search.length < 20 ? "?" + match.search : "") + + (match.hash.length < 20 ? match.hash : "")} ); } diff --git a/src/components/note/text-note-contents.tsx b/src/components/note/text-note-contents.tsx index dabfb5ebc..a0a53c642 100644 --- a/src/components/note/text-note-contents.tsx +++ b/src/components/note/text-note-contents.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Box, BoxProps } from "@chakra-ui/react"; +import React, { Suspense } from "react"; +import { Box, BoxProps, Spinner } from "@chakra-ui/react"; import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../helpers/embeds"; @@ -90,9 +90,11 @@ export const NoteContents = React.memo( return ( - - {content} - + }> + + {content} + + ); }, diff --git a/src/components/timeline-page/generic-note-timeline/index.tsx b/src/components/timeline-page/generic-note-timeline/index.tsx index 3e9e4eed4..1c12283df 100644 --- a/src/components/timeline-page/generic-note-timeline/index.tsx +++ b/src/components/timeline-page/generic-note-timeline/index.tsx @@ -18,7 +18,7 @@ const NOTE_BUFFER = 5; const timelineNoteMinHeightCache = new WeakMap>>(); function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) { - const events = useThrottle(useSubject(timeline.timeline), 100); + const events = useSubject(timeline.timeline); const [latest, setLatest] = useState(() => dayjs().unix()); const location = useLocation(); diff --git a/src/helpers/nostr/filter.ts b/src/helpers/nostr/filter.ts index 1771e9a45..9ab530a02 100644 --- a/src/helpers/nostr/filter.ts +++ b/src/helpers/nostr/filter.ts @@ -1,6 +1,6 @@ import stringify from "json-stringify-deterministic"; import { NostrQuery, NostrRequestFilter, RelayQueryMap } from "../../types/nostr-query"; -import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "../../services/local-cache-relay"; +import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "../../services/local-relay"; export function addQueryToFilter(filter: NostrRequestFilter, query: NostrQuery) { if (Array.isArray(filter)) { diff --git a/src/services/channel-metadata.ts b/src/services/channel-metadata.ts index 95e96bde4..396cccaae 100644 --- a/src/services/channel-metadata.ts +++ b/src/services/channel-metadata.ts @@ -12,7 +12,7 @@ import { logger } from "../helpers/debug"; import db from "./db"; import createDefer, { Deferred } from "../classes/deferred"; import { getChannelPointer } from "../helpers/nostr/channel"; -import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-cache-relay"; +import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-relay"; type Pubkey = string; type Relay = string; diff --git a/src/services/db/index.ts b/src/services/db/index.ts index cf9930bcb..b0f9f710b 100644 --- a/src/services/db/index.ts +++ b/src/services/db/index.ts @@ -3,7 +3,7 @@ import { clearDB, deleteDB as nostrIDBDelete } from "nostr-idb"; import { SchemaV1, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7, SchemaV8 } from "./schema"; import { logger } from "../../helpers/debug"; -import { localCacheDatabase } from "../local-cache-relay"; +import { localDatabase } from "../local-relay"; const log = logger.extend("Database"); @@ -178,7 +178,7 @@ log("Open"); export async function clearCacheData() { log("Clearing nostr-idb"); - await clearDB(localCacheDatabase); + await clearDB(localDatabase); log("Clearing channelMetadata"); await db.clear("channelMetadata"); diff --git a/src/services/event-exists.ts b/src/services/event-exists.ts index 73cdb2e48..cc8cb8725 100644 --- a/src/services/event-exists.ts +++ b/src/services/event-exists.ts @@ -8,7 +8,7 @@ import relayScoreboardService from "./relay-scoreboard"; import { logger } from "../helpers/debug"; import { matchFilter, matchFilters } from "nostr-tools"; import { NostrEvent } from "../types/nostr-event"; -import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-cache-relay"; +import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-relay"; function hashFilter(filter: NostrRequestFilter) { return stringify(filter); diff --git a/src/services/local-cache-relay.ts b/src/services/local-relay.ts similarity index 58% rename from src/services/local-cache-relay.ts rename to src/services/local-relay.ts index 09dacc3a3..93fd04ead 100644 --- a/src/services/local-cache-relay.ts +++ b/src/services/local-relay.ts @@ -3,25 +3,25 @@ import { Relay } from "nostr-tools"; import { logger } from "../helpers/debug"; import _throttle from "lodash.throttle"; -const log = logger.extend(`LocalCacheRelay`); +const log = logger.extend(`LocalRelay`); const params = new URLSearchParams(location.search); -const paramRelay = params.get("cacheRelay"); +const paramRelay = params.get("localRelay"); // save the cache relay to localStorage if (paramRelay) { - localStorage.setItem("cacheRelay", paramRelay); - params.delete("cacheRelay"); + localStorage.setItem("localRelay", paramRelay); + params.delete("localRelay"); if (params.size === 0) location.search = params.toString(); } -const storedCacheRelayURL = localStorage.getItem("cacheRelay"); -const url = (storedCacheRelayURL && new URL(storedCacheRelayURL)) || new URL("/cache-relay", location.href); +const storedCacheRelayURL = localStorage.getItem("localRelay"); +const url = (storedCacheRelayURL && new URL(storedCacheRelayURL)) || new URL("/local-relay", location.href); url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; -export const LOCAL_CACHE_RELAY_ENABLED = !!window.CACHE_RELAY_ENABLED || !!localStorage.getItem("cacheRelay"); +export const LOCAL_CACHE_RELAY_ENABLED = !!window.CACHE_RELAY_ENABLED || !!localStorage.getItem("localRelay"); export const LOCAL_CACHE_RELAY = url.toString(); -export const localCacheDatabase = await openDB(); +export const localDatabase = await openDB(); function createRelay() { if (LOCAL_CACHE_RELAY_ENABLED) { @@ -29,19 +29,19 @@ function createRelay() { return new Relay(LOCAL_CACHE_RELAY); } else { log(`Using IndexedDB`); - return new CacheRelay(localCacheDatabase); + return new CacheRelay(localDatabase, { maxEvents: 10000 }); } } -export const localCacheRelay = createRelay(); +export const localRelay = createRelay(); function pruneLocalDatabase() { - if (localCacheRelay instanceof CacheRelay) { - pruneLastUsed(localCacheRelay.db, 20_000); + if (localRelay instanceof CacheRelay) { + pruneLastUsed(localRelay.db, 20_000); } } // connect without waiting -localCacheRelay.connect().then(() => { +localRelay.connect().then(() => { log("Connected"); pruneLocalDatabase(); @@ -49,7 +49,7 @@ localCacheRelay.connect().then(() => { // keep the relay connection alive setInterval(() => { - if (!localCacheRelay.connected) localCacheRelay.connect().then(() => log("Reconnected")); + if (!localRelay.connected) localRelay.connect().then(() => log("Reconnected")); }, 1000 * 5); setInterval(() => { @@ -58,5 +58,7 @@ setInterval(() => { if (import.meta.env.DEV) { //@ts-ignore - window.localCacheRelay = localCacheRelay; + window.localDatabase = localDatabase; + //@ts-ignore + window.localRelay = localRelay; } diff --git a/src/services/relay-stats.ts b/src/services/relay-stats.ts index 25e1acd41..876886413 100644 --- a/src/services/relay-stats.ts +++ b/src/services/relay-stats.ts @@ -6,7 +6,7 @@ import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; import relayInfoService from "./relay-info"; import { normalizeRelayURL } from "../helpers/relay"; -import { localCacheRelay } from "./local-cache-relay"; +import { localRelay } from "./local-relay"; import { MONITOR_STATS_KIND, SELF_REPORTED_KIND, getRelayURL } from "../helpers/nostr/relay-stats"; const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f"; @@ -18,7 +18,7 @@ class RelayStatsService { constructor() { // load all stats from cache and subscribe to future ones - localCacheRelay.subscribe([{ kinds: [SELF_REPORTED_KIND, MONITOR_STATS_KIND] }], { + localRelay.subscribe([{ kinds: [SELF_REPORTED_KIND, MONITOR_STATS_KIND] }], { onevent: (e) => this.handleEvent(e, false), }); } @@ -34,12 +34,12 @@ class RelayStatsService { if (event.kind === SELF_REPORTED_KIND) { if (!sub.value || event.created_at > sub.value.created_at) { sub.next(event); - if (cache) localCacheRelay.publish(event); + if (cache) localRelay.publish(event); } } else if (event.kind === MONITOR_STATS_KIND) { if (!sub.value || event.created_at > sub.value.created_at) { sub.next(event); - if (cache) localCacheRelay.publish(event); + if (cache) localRelay.publish(event); } } } diff --git a/src/services/replaceable-event-requester.ts b/src/services/replaceable-event-requester.ts index 39f1f12a2..75ef442dc 100644 --- a/src/services/replaceable-event-requester.ts +++ b/src/services/replaceable-event-requester.ts @@ -12,7 +12,7 @@ import db from "./db"; import { nameOrPubkey } from "./user-metadata"; import { getEventCoordinate, parseCoordinate } from "../helpers/nostr/events"; import createDefer, { Deferred } from "../classes/deferred"; -import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED, localCacheRelay } from "./local-cache-relay"; +import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED, localRelay } from "./local-relay"; import { relayRequest } from "../helpers/relay"; type Pubkey = string; @@ -199,7 +199,7 @@ class ReplaceableEventLoaderService { } const filters = Array.from(Object.values(kindFilters)); - const events = await relayRequest(localCacheRelay, filters); + const events = await relayRequest(localRelay, filters); for (const event of events) { this.handleEvent(event, false); const cord = getEventCoordinate(event); @@ -235,7 +235,7 @@ class ReplaceableEventLoaderService { if (this.writeCacheQueue.size === 0) return; this.dbLog(`Writing ${this.writeCacheQueue.size} events to database`); - for (const [_, event] of this.writeCacheQueue) localCacheRelay.publish(event); + for (const [_, event] of this.writeCacheQueue) localRelay.publish(event); this.writeCacheQueue.clear(); } private async saveToCache(cord: string, event: NostrEvent) { @@ -248,7 +248,7 @@ class ReplaceableEventLoaderService { const sub = this.events.get(cord); const relayUrls = Array.from(relays); - // TODO: use localCacheRelay instead + // TODO: use localRelay instead if (LOCAL_CACHE_RELAY_ENABLED) relayUrls.unshift(LOCAL_CACHE_RELAY); for (const relay of relayUrls) { diff --git a/src/services/single-event.ts b/src/services/single-event.ts index 50fef4b10..ed187ae28 100644 --- a/src/services/single-event.ts +++ b/src/services/single-event.ts @@ -4,7 +4,7 @@ import NostrRequest from "../classes/nostr-request"; import Subject from "../classes/subject"; import SuperMap from "../classes/super-map"; import { NostrEvent } from "../types/nostr-event"; -import { localCacheRelay } from "./local-cache-relay"; +import { localRelay } from "./local-relay"; import { relayRequest, safeRelayUrls } from "../helpers/relay"; import { logger } from "../helpers/debug"; @@ -29,7 +29,7 @@ class SingleEventService { handleEvent(event: NostrEvent, cache = true) { this.cache.get(event.id).next(event); - if (cache) localCacheRelay.publish(event); + if (cache) localRelay.publish(event); } private batchRequestsThrottle = _throttle(this.batchRequests, RELAY_REQUEST_BATCH_TIME); @@ -40,7 +40,7 @@ class SingleEventService { const loaded: string[] = []; // load from cache relay - const fromCache = await relayRequest(localCacheRelay, [{ ids }]); + const fromCache = await relayRequest(localRelay, [{ ids }]); for (const e of fromCache) { this.handleEvent(e, false); diff --git a/src/views/settings/database-settings.tsx b/src/views/settings/database-settings.tsx index 327613581..56d0baae8 100644 --- a/src/views/settings/database-settings.tsx +++ b/src/views/settings/database-settings.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; +import { useAsync } from "react-use"; import { Button, AccordionItem, @@ -8,17 +9,19 @@ import { AccordionIcon, ButtonGroup, Text, + Input, } from "@chakra-ui/react"; -import { useAsync } from "react-use"; -import { countEvents, countEventsByKind } from "nostr-idb"; +import { addEvents, countEvents, countEventsByKind, getEventUID, updateUsed } from "nostr-idb"; +import stringify from "json-stringify-deterministic"; import { clearCacheData, deleteDatabase } from "../../services/db"; import { DatabaseIcon } from "../../components/icons"; -import { localCacheDatabase } from "../../services/local-cache-relay"; +import { localDatabase } from "../../services/local-relay"; +import { NostrEvent } from "../../types/nostr-event"; function DatabaseStats() { - const { value: count } = useAsync(async () => await countEvents(localCacheDatabase), []); - const { value: kinds } = useAsync(async () => await countEventsByKind(localCacheDatabase), []); + const { value: count } = useAsync(async () => await countEvents(localDatabase), []); + const { value: kinds } = useAsync(async () => await countEventsByKind(localDatabase), []); return ( <> @@ -32,6 +35,49 @@ function DatabaseStats() { ); } +function ImportButton() { + const ref = useRef(null); + const [importing, setImporting] = useState(false); + const importFile = (file: File) => { + setImporting(true); + const reader = new FileReader(); + reader.readAsText(file, "utf8"); + reader.onload = async () => { + if (typeof reader.result !== "string") return; + const lines = reader.result.split("\n"); + const events: NostrEvent[] = []; + for (const line of lines) { + try { + const event = JSON.parse(line) as NostrEvent; + events.push(event); + } catch (e) {} + } + await addEvents(localDatabase, events); + await updateUsed( + localDatabase, + events.map((e) => getEventUID(e)), + ); + alert(`Imported ${events.length} events`); + setImporting(false); + }; + }; + + return ( + <> + e.target.files?.[0] && importFile(e.target.files[0])} + ref={ref} + /> + + + ); +} + export default function DatabaseSettings() { const [clearing, setClearing] = useState(false); const handleClearData = async () => { @@ -47,6 +93,19 @@ export default function DatabaseSettings() { setDeleting(false); }; + const [exporting, setExporting] = useState(false); + const exportDatabase = async () => { + setExporting(true); + const rows = await localDatabase.getAll("events"); + const lines = rows.map((row) => stringify(row.event)); + const file = new File(lines, "noStrudel-export.jsonl", { + type: "application/jsonl", + }); + const url = URL.createObjectURL(file); + window.open(url); + setExporting(false); + }; + return (

@@ -61,10 +120,14 @@ export default function DatabaseSettings() { - - + diff --git a/src/views/settings/performance-settings.tsx b/src/views/settings/performance-settings.tsx index 87bf348db..f74fc6662 100644 --- a/src/views/settings/performance-settings.tsx +++ b/src/views/settings/performance-settings.tsx @@ -28,12 +28,9 @@ import { import { safeUrl } from "../../helpers/parse"; import { AppSettings } from "../../services/settings/migrations"; import { PerformanceIcon } from "../../components/icons"; -import { useLocalStorage } from "react-use"; -import { LOCAL_CACHE_RELAY } from "../../services/local-cache-relay"; export default function PerformanceSettings() { const { register, formState } = useFormContext(); - const [localCacheRelay, setLocalCacheRelay] = useLocalStorage("enable-cache-relay"); const cacheDetails = useDisclosure(); return ( @@ -129,7 +126,7 @@ export default function PerformanceSettings() { - When this is enabled noStrudel will connect to the relay at ws://{""}/cache-relay and + When this is enabled noStrudel will connect to the relay at ws://{""}/local-relay and use it to cache all events it finds.