Browse Source

Merge remote-tracking branch 'origin/master' into feature/text-entry

master
limina1 8 months ago
parent
commit
9507a5e25e
  1. 10
      .cursor/rules/alexandria.mdc
  2. 2
      Dockerfile
  3. 15
      Dockerfile.next
  4. 15
      Dockerfile.prod
  5. 33
      README.md
  6. 1078
      bun.lock
  7. 1
      deno.json
  8. 1289
      deno.lock
  9. 4
      docker-compose.yaml
  10. 16
      import_map.json
  11. 33
      package.json
  12. 1
      playwright.config.ts
  13. 17
      src/app.d.ts
  14. 22
      src/lib/components/EventInput.svelte
  15. 6
      src/lib/components/publications/PublicationFeed.svelte
  16. 2
      src/lib/components/util/CardActions.svelte
  17. 338
      src/lib/data_structures/websocket_pool.ts
  18. 3
      src/lib/navigator/EventNetwork/utils/forceSimulation.ts
  19. 8
      src/lib/navigator/EventNetwork/utils/networkBuilder.ts
  20. 108
      src/lib/ndk.ts
  21. 16
      src/lib/parser.ts
  22. 10
      src/lib/services/publisher.ts
  23. 2
      src/lib/state.ts
  24. 4
      src/lib/stores.ts
  25. 4
      src/lib/stores/networkStore.ts
  26. 27
      src/lib/stores/userStore.ts
  27. 2
      src/lib/types.ts
  28. 8
      src/lib/utils.ts
  29. 8
      src/lib/utils/ZettelParser.ts
  30. 35
      src/lib/utils/community_checker.ts
  31. 13
      src/lib/utils/event_input_utils.ts
  32. 18
      src/lib/utils/event_search.ts
  33. 5
      src/lib/utils/indexEventCache.ts
  34. 14
      src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts
  35. 24
      src/lib/utils/markup/advancedMarkupParser.ts
  36. 25
      src/lib/utils/markup/asciidoctorExtensions.ts
  37. 8
      src/lib/utils/markup/basicMarkupParser.ts
  38. 11
      src/lib/utils/network_detection.ts
  39. 21
      src/lib/utils/nostrEventService.ts
  40. 26
      src/lib/utils/nostrUtils.ts
  41. 30
      src/lib/utils/profile_search.ts
  42. 73
      src/lib/utils/relayDiagnostics.ts
  43. 34
      src/lib/utils/relay_management.ts
  44. 5
      src/lib/utils/searchCache.ts
  45. 6
      src/lib/utils/search_types.ts
  46. 7
      src/lib/utils/search_utils.ts
  47. 46
      src/lib/utils/subscription_search.ts
  48. 15
      src/routes/+layout.ts
  49. 6
      src/routes/+page.svelte
  50. 15
      src/routes/publication/+page.ts
  51. 3
      vite.config.ts

10
.cursor/rules/alexandria.mdc

@ -19,6 +19,12 @@ In reader mode, Alexandria loads a document tree from a root publication index e @@ -19,6 +19,12 @@ In reader mode, Alexandria loads a document tree from a root publication index e
Svelte components in Alexandria use TypeScript exclusively over plain JavaScript. Styles are defined via Tailwind 4 utility classes, and some custom utility classes are defined in [app.css](mdc:src/app.css). The app runs on Deno, but maintains compatibility with Node.js.
### Utilities
The project contains a number of modules that define utility classes and functions to support the app's operations. Make use of these utilities when they are relevant to reduce code duplication and maintain common patterns:
- Use the `WebSocketPool` class defined in [websocket_pool.ts](../src/lib/data_structures/websocket_pool.ts) when handling raw WebSockets to efficiently manage connections.
## General Guidelines
When responding to prompts, adhere to the following rules:
@ -49,7 +55,7 @@ Observe the following style guidelines when writing code: @@ -49,7 +55,7 @@ Observe the following style guidelines when writing code:
- Use blocks enclosed by curly brackets when writing control flow expressions such as `for` and `while` loops, and `if` and `switch` statements.
- Begin `case` expressions in a `switch` statement at the same indentation level as the `switch` itself. Indent code within a `case` block.
- Limit line length to 100 characters; break statements across lines if necessary.
- Default to single quotes.
- Default to double quotes.
### HTML
@ -57,5 +63,3 @@ Observe the following style guidelines when writing code: @@ -57,5 +63,3 @@ Observe the following style guidelines when writing code:
- Break long tags across multiple lines.
- Use Tailwind 4 utility classes for styling.
- Default to single quotes.

2
Dockerfile.local → Dockerfile

@ -11,5 +11,7 @@ COPY --from=build /app/src/import_map.json . @@ -11,5 +11,7 @@ COPY --from=build /app/src/import_map.json .
ENV ORIGIN=http://localhost:3000
RUN deno cache --import-map=import_map.json ./build/index.js
EXPOSE 3000
CMD [ "deno", "run", "--allow-env", "--allow-read", "--allow-net", "--import-map=import_map.json", "./build/index.js" ]

15
Dockerfile.next

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
FROM denoland/deno:alpine AS build
WORKDIR /app/src
COPY . .
RUN deno install
RUN deno task build
FROM denoland/deno:alpine AS release
WORKDIR /app
COPY --from=build /app/src/build/ ./build/
COPY --from=build /app/src/import_map.json .
ENV ORIGIN=https://$HOST
EXPOSE 3000
CMD [ "deno", "run", "--allow-env", "--allow-read", "--allow-net", "--import-map=import_map.json", "./build/index.js" ]

15
Dockerfile.prod

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
FROM denoland/deno:alpine AS build
WORKDIR /app/src
COPY . .
RUN deno install
RUN deno task build
FROM denoland/deno:alpine AS release
WORKDIR /app
COPY --from=build /app/src/build/ ./build/
COPY --from=build /app/src/import_map.json .
ENV ORIGIN=https://$HOST
EXPOSE 3000
CMD [ "deno", "run", "--allow-env", "--allow-read", "--allow-net", "--import-map=import_map.json", "./build/index.js" ]

33
README.md

@ -69,37 +69,6 @@ or with Deno: @@ -69,37 +69,6 @@ or with Deno:
deno task preview
```
## Docker
This docker container performs the build.
To build the container:
```bash
docker build . -t gc-alexandria
```
To run the container, in detached mode (-d):
```bash
docker run -d --rm --name=gc-alexandria -p 4174:80 gc-alexandria
```
The container is then viewable on your [local machine](http://localhost:4173).
If you want to see the container process (assuming it's the last process to start), enter:
```bash
docker ps -l
```
which should return something like:
```bash
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1d83d736322f gc-alexandria "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:4174->80/tcp, [::]:4174->80/tcp gc-alexandria
```
## Docker + Deno
This application is configured to use the Deno runtime. A Docker container is provided to handle builds and deployments.
@ -107,7 +76,7 @@ This application is configured to use the Deno runtime. A Docker container is pr @@ -107,7 +76,7 @@ This application is configured to use the Deno runtime. A Docker container is pr
To build the app for local development:
```bash
docker build -t local-alexandria -f Dockerfile.local .
docker build -t local-alexandria -f Dockerfile .
```
To run the local development build:

1078
bun.lock

File diff suppressed because it is too large Load Diff

1
deno.json

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
{
"importMap": "./import_map.json",
"compilerOptions": {
"allowJs": true,
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
}
}

1289
deno.lock

File diff suppressed because it is too large Load Diff

4
docker-compose.yaml

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
version: "3"
services:
wikinostr:
alexandria:
build:
context: .
dockerfile: Dockerfile
ports:
- 3023:4173
- 3000:3000

16
import_map.json

@ -1,19 +1,19 @@ @@ -1,19 +1,19 @@
{
"imports": {
"he": "npm:he@1.2.x",
"@nostr-dev-kit/ndk": "npm:@nostr-dev-kit/ndk@2.11.x",
"@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@2.5.x",
"@nostr-dev-kit/ndk": "npm:@nostr-dev-kit/ndk@^2.14.32",
"@nostr-dev-kit/ndk-cache-dexie": "npm:@nostr-dev-kit/ndk-cache-dexie@^2.6.33",
"@popperjs/core": "npm:@popperjs/core@2.11.x",
"@tailwindcss/forms": "npm:@tailwindcss/forms@0.5.x",
"@tailwindcss/typography": "npm:@tailwindcss/typography@0.5.x",
"asciidoctor": "npm:asciidoctor@3.0.x",
"d3": "npm:d3@7.9.x",
"nostr-tools": "npm:nostr-tools@2.10.x",
"tailwind-merge": "npm:tailwind-merge@2.5.x",
"svelte": "npm:svelte@5.0.x",
"flowbite": "npm:flowbite@2.2.x",
"flowbite-svelte": "npm:flowbite-svelte@0.48.x",
"flowbite-svelte-icons": "npm:flowbite-svelte-icons@2.1.x",
"nostr-tools": "npm:nostr-tools@^2.15.1",
"tailwind-merge": "npm:tailwind-merge@^3.3.1",
"svelte": "npm:svelte@^5.36.8",
"flowbite": "npm:flowbite@^3.1.2",
"flowbite-svelte": "npm:flowbite-svelte@^1.10.10",
"flowbite-svelte-icons": "npm:flowbite-svelte-icons@^2.2.1",
"child_process": "node:child_process"
}
}

33
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "alexandria",
"version": "0.0.6",
"version": "0.0.2",
"private": true,
"type": "module",
"scripts": {
@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
"test": "vitest"
},
"dependencies": {
"@noble/hashes": "^1.8.0",
"@noble/curves": "^1.9.4",
"@nostr-dev-kit/ndk": "^2.14.32",
"@nostr-dev-kit/ndk-cache-dexie": "2.6.x",
"@popperjs/core": "2.11.x",
@ -30,33 +32,34 @@ @@ -30,33 +32,34 @@
"qrcode": "^1.5.4"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@sveltejs/adapter-auto": "3.x",
"@playwright/test": "^1.54.1",
"@sveltejs/adapter-auto": "^6.0.1",
"@sveltejs/adapter-node": "^5.2.13",
"@sveltejs/adapter-static": "3.x",
"@sveltejs/kit": "^2.25.0",
"@sveltejs/vite-plugin-svelte": "5.x",
"@sveltejs/vite-plugin-svelte": "^6.1.0",
"@types/d3": "^7.4.3",
"@types/he": "1.2.x",
"@types/node": "22.x",
"@types/mathjax": "^0.0.40",
"@types/node": "^24.0.15",
"@types/qrcode": "^1.5.5",
"autoprefixer": "10.x",
"eslint-plugin-svelte": "2.x",
"autoprefixer": "^10.4.21",
"eslint-plugin-svelte": "^3.11.0",
"flowbite": "2.x",
"flowbite-svelte": "0.48.x",
"flowbite-svelte-icons": "2.1.x",
"playwright": "^1.50.1",
"postcss": "8.x",
"postcss": "^8.5.6",
"postcss-load-config": "6.x",
"prettier": "3.x",
"prettier-plugin-svelte": "3.x",
"svelte": "5.x",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.36.8",
"svelte-check": "4.x",
"tailwind-merge": "^3.3.0",
"tailwindcss": "3.x",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^3.4.17",
"tslib": "2.8.x",
"typescript": "5.8.x",
"vite": "6.x",
"typescript": "^5.8.3",
"vite": "^7.0.5",
"vitest": "^3.1.3"
}
}

1
playwright.config.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import process from "node:process";
/**
* Read environment variables from file.

17
src/app.d.ts vendored

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
// See https://kit.svelte.dev/docs/types#app
import NDK, { NDKEvent } from "@nostr-dev-kit/ndk";
import Pharos from "./lib/parser.ts";
import { NDKEvent, NDKNip07Signer } from "@nostr-dev-kit/ndk";
import { HLJSApi } from "highlight.js";
// for information about these interfaces
declare global {
@ -9,13 +9,24 @@ declare global { @@ -9,13 +9,24 @@ declare global {
// interface Error {}
// interface Locals {}
interface PageData {
waitable?: Promise<any>;
waitable?: Promise<unknown>;
publicationType?: string;
indexEvent?: NDKEvent;
url?: URL;
}
// interface Platform {}
}
var hljs: HLJSApi;
// deno-lint-ignore no-explicit-any
var MathJax: any;
var nostr: NDKNip07Signer & {
getRelays: () => Promise<Record<string, Record<string, boolean | undefined>>>;
// deno-lint-ignore no-explicit-any
signEvent: (event: any) => Promise<any>;
};
}
export {};

22
src/lib/components/EventInput.svelte

@ -3,7 +3,6 @@ @@ -3,7 +3,6 @@
getTitleTagForEvent,
getDTagForEvent,
requiresDTag,
hasDTag,
validateNotAsciidoc,
validateAsciiDoc,
build30040EventSet,
@ -22,8 +21,8 @@ @@ -22,8 +21,8 @@
import { prefixNostrAddresses } from "$lib/utils/nostrUtils";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { Button } from "flowbite-svelte";
import { nip19 } from "nostr-tools";
import { goto } from "$app/navigation";
import { WebSocketPool } from "$lib/data_structures/websocket_pool";
let kind = $state<number>(30023);
let tags = $state<[string, string][]>([]);
@ -308,17 +307,14 @@ @@ -308,17 +307,14 @@
for (const relayUrl of relays) {
try {
const ws = new WebSocket(relayUrl);
const ws = await WebSocketPool.instance.acquire(relayUrl);
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
WebSocketPool.instance.release(ws);
reject(new Error("Timeout"));
}, 5000);
ws.onopen = () => {
ws.send(JSON.stringify(["EVENT", signedEvent]));
};
ws.onmessage = (e) => {
const [type, id, ok, message] = JSON.parse(e.data);
if (type === "OK" && id === signedEvent.id) {
@ -326,20 +322,14 @@ @@ -326,20 +322,14 @@
if (ok) {
published = true;
relaysPublished.push(relayUrl);
ws.close();
WebSocketPool.instance.release(ws);
resolve();
} else {
ws.close();
WebSocketPool.instance.release(ws);
reject(new Error(message));
}
}
};
ws.onerror = () => {
clearTimeout(timeout);
ws.close();
reject(new Error("WebSocket error"));
};
});
if (published) break;
} catch (e) {

6
src/lib/components/publications/PublicationFeed.svelte

@ -35,10 +35,6 @@ @@ -35,10 +35,6 @@
// Event management
let allIndexEvents: NDKEvent[] = $state([]);
let cutoffTimestamp: number = $derived(
eventsInView?.at(eventsInView.length - 1)?.created_at ??
new Date().getTime(),
);
// Initialize relays and fetch events
async function initializeAndFetch() {
@ -371,8 +367,6 @@ @@ -371,8 +367,6 @@
</script>
<div class="flex flex-col space-y-4">
<div
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 w-full"
>

2
src/lib/components/util/CardActions.svelte

@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { neventEncode, naddrEncode } from "$lib/utils";
import { activeInboxRelays } from "$lib/ndk";
import { userStore } from "$lib/stores/userStore";
import { userStore } from "$lib/stores/userStore";
import { goto } from "$app/navigation";
import type { NDKEvent } from "$lib/utils/nostrUtils";

338
src/lib/data_structures/websocket_pool.ts

@ -0,0 +1,338 @@ @@ -0,0 +1,338 @@
/**
* A representation of a WebSocket connection used internally by the WebSocketPool. Attaches
* information about the connection's state within the resource pool to the connection itself.
*/
interface WebSocketHandle {
ws: WebSocket;
refCount: number;
idleTimer?: ReturnType<typeof setTimeout>;
}
interface WebSocketPoolWaitingQueueItem {
url: string;
resolve: (handle: WebSocketHandle) => void;
reject: (reason?: any) => void;
}
/**
* A resource pool for WebSocket connections. Its purpose is to allow multiple requestors to share
* an open WebSocket connection for greater resource efficiency.
*
* The pool maintains a single connection per URL. Requestors may acquire a reference to a
* connection, and are expected to release it when it is no longer needed. If the number of
* requestors using a connection drops to zero and no new requestors acquire it for a set period,
* the connection is closed.
*/
export class WebSocketPool {
static #shared: WebSocketPool;
/**
* A map of WebSocket URLs to the handles containing the connection and its state.
*/
#pool: Map<string, WebSocketHandle> = new Map();
#idleTimeoutMs: number;
#maxConnections: number;
#waitingQueue: WebSocketPoolWaitingQueueItem[] = [];
/**
* Private constructor invoked when the singleton instance is first created.
* @param idleTimeoutMs - The timeout in milliseconds after which idle connections will be
* closed. Defaults to 60 seconds.
* @param maxConnections - The maximum number of simultaneous WebSocket connections. Defaults to
* 16.
*/
private constructor(idleTimeoutMs: number = 60000, maxConnections: number = 16) {
this.#idleTimeoutMs = idleTimeoutMs;
this.#maxConnections = maxConnections;
}
/**
* Returns the singleton instance of the WebsocketPool.
* @returns The singleton instance.
*/
public static get instance(): WebSocketPool {
if (!WebSocketPool.#shared) {
WebSocketPool.#shared = new WebSocketPool();
}
return WebSocketPool.#shared;
}
// #region Resource Management Interface
/**
* Sets the maximum number of simultaneous WebSocket connections.
*
* @param limit - The new connection limit.
*/
public set maxConnections(limit: number) {
if (limit === this.#maxConnections) {
return;
}
if (limit == null || isNaN(limit)) {
throw new Error('[WebSocketPool] Connection limit must be a number.');
}
if (limit <= 0) {
throw new Error('[WebSocketPool] Connection limit must be greater than 0.');
}
if (!Number.isInteger(limit)) {
throw new Error('[WebSocketPool] Connection limit must be an integer.');
}
this.#maxConnections = limit;
this.#processWaitingQueue();
}
/**
* Gets the current maximum number of simultaneous WebSocket connections.
*
* @returns The current connection limit.
*/
public get maxConnections(): number {
return this.#maxConnections;
}
/**
* Sets the idle timeout for WebSocket connections.
*
* @param timeoutMs - The timeout in milliseconds after which idle connections will be closed.
*/
public set idleTimeoutMs(timeoutMs: number) {
if (timeoutMs === this.#idleTimeoutMs) {
return;
}
if (timeoutMs == null || isNaN(timeoutMs)) {
throw new Error('[WebSocketPool] Idle timeout must be a number.');
}
if (timeoutMs <= 0) {
throw new Error('[WebSocketPool] Idle timeout must be greater than 0.');
}
if (!Number.isInteger(timeoutMs)) {
throw new Error('[WebSocketPool] Idle timeout must be an integer.');
}
this.#idleTimeoutMs = timeoutMs;
}
/**
* Gets the current idle timeout setting.
*
* @returns The current idle timeout in milliseconds.
*/
public get idleTimeoutMs(): number {
return this.#idleTimeoutMs;
}
/**
* Acquires a WebSocket connection for the specified URL. If a connection is available in the
* pool, that connection is returned. If no connection is available but the pool is not full, a
* new connection is created and returned. If the pool is full, the request is queued until a
* connection is released, at which point the newly-available connection is returned to the
* caller.
*
* @param url - The URL to connect to.
* @returns A promise that resolves with a WebSocket connection.
*/
public async acquire(url: string): Promise<WebSocket> {
const normalizedUrl = this.#normalizeUrl(url);
const handle = this.#pool.get(normalizedUrl);
try {
if (handle && handle.ws.readyState === WebSocket.OPEN) {
this.#checkOut(handle);
return handle.ws;
}
if (this.#pool.size >= this.#maxConnections) {
return new Promise((resolve, reject) => {
this.#waitingQueue.push({
url: normalizedUrl,
resolve: (handle) => resolve(handle.ws),
reject,
});
});
}
const newHandle = await this.#createSocket(normalizedUrl);
return newHandle.ws;
} catch (error) {
throw new Error(
`[WebSocketPool] Failed to acquire connection for ${normalizedUrl}: ${error}`
);
}
}
/**
* Releases a WebSocket connection back to the pool. If there are pending requests for the same
* URL, the connection is passed to the requestor in the queue. Otherwise, the connection is
* marked as available.
*
* @param handle - The WebSocketHandle to release.
*/
public release(ws: WebSocket): void {
const normalizedUrl = this.#normalizeUrl(ws.url);
const handle = this.#pool.get(normalizedUrl);
if (!handle) {
throw new Error('[WebSocketPool] Attempted to release an unmanaged WebSocket connection.');
}
if (--handle.refCount === 0) {
this.#startIdleTimer(handle);
}
}
/**
* Closes all WebSocket connections and "drains" the pool.
*/
public drain(): void {
// Clear all idle timers first
for (const handle of this.#pool.values()) {
this.#clearIdleTimer(handle);
}
for (const { reject } of this.#waitingQueue) {
reject(new Error('[WebSocketPool] Draining pool.'));
}
this.#waitingQueue = [];
for (const handle of this.#pool.values()) {
handle.ws.close();
}
this.#pool.clear();
}
// #endregion
// #region Private Helper Methods
#createSocket(url: string): Promise<WebSocketHandle> {
return new Promise((resolve, reject) => {
try {
const handle: WebSocketHandle = {
ws: new WebSocket(url),
refCount: 1,
};
handle.ws.onopen = () => {
this.#pool.set(url, handle);
// Remove the socket from the pool when it is closed. The socket may be closed by
// either the client or the server.
handle.ws.onclose = () => this.#removeSocket(handle);
resolve(handle);
};
handle.ws.onerror = (event) => {
this.#removeSocket(handle);
this.#processWaitingQueue();
reject(
new Error(`[WebSocketPool] WebSocket connection failed for ${url}: ${event.type}`)
);
};
} catch (error) {
this.#processWaitingQueue();
reject(error);
}
});
}
#removeSocket(handle: WebSocketHandle): void {
this.#clearIdleTimer(handle);
handle.ws.onopen = handle.ws.onerror = handle.ws.onclose = null;
this.#pool.delete(this.#normalizeUrl(handle.ws.url));
this.#processWaitingQueue();
}
/**
* Starts an idle timer for the specified WebSocket. The connection will be automatically
* closed after the idle timeout period if it remains unused.
*
* @param ws - The WebSocket for which to start the idle timer.
*/
#startIdleTimer(handle: WebSocketHandle): void {
// Clear any existing timer first
this.#clearIdleTimer(handle);
handle.idleTimer = setTimeout(() => {
const refCount = handle.refCount;
if (refCount === 0 && handle.ws.readyState === WebSocket.OPEN) {
handle.ws.close();
this.#removeSocket(handle);
}
}, this.#idleTimeoutMs);
}
/**
* Clears the idle timer for the specified WebSocket.
*
* @param handle - The WebSocketHandle for which to clear the idle timer.
*/
#clearIdleTimer(handle: WebSocketHandle): void {
const timer = handle.idleTimer;
if (timer) {
clearTimeout(timer);
handle.idleTimer = undefined;
}
}
/**
* Processes pending requests to acquire a connection. Reuses existing connections when possible.
*/
#processWaitingQueue(): void {
while (
this.#waitingQueue.length > 0 &&
this.#pool.size < this.#maxConnections
) {
const nextInQueue = this.#waitingQueue.shift();
if (!nextInQueue) {
continue;
}
const { url, resolve, reject } = nextInQueue;
const existingHandle = this.#pool.get(url);
if (existingHandle && existingHandle.ws.readyState === WebSocket.OPEN) {
this.#checkOut(existingHandle);
resolve(existingHandle);
return;
}
this.#createSocket(url).then(resolve, reject);
}
}
#checkOut(handle: WebSocketHandle): void {
if (handle.refCount == null) {
throw new Error('[WebSocketPool] Handle refCount unexpectedly null.');
}
++handle.refCount;
this.#clearIdleTimer(handle);
}
#normalizeUrl(url: string): string {
try {
const urlObj = new URL(url);
// The URL constructor correctly normalizes scheme and hostname casing.
let normalized = urlObj.toString();
// The logic to remove a trailing slash for connection coalescing can be kept,
// but should be done on the normalized string.
if (urlObj.pathname !== '/' && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
} catch {
// If URL is invalid, return it as-is and let WebSocket constructor handle the error.
return url;
}
}
// #endregion
}

3
src/lib/navigator/EventNetwork/utils/forceSimulation.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
// deno-lint-ignore-file no-explicit-any
/**
* D3 Force Simulation Utilities
*
@ -5,7 +6,7 @@ @@ -5,7 +6,7 @@
* graph simulations for the event network visualization.
*/
import type { NetworkNode, NetworkLink } from "../types";
import type { NetworkNode, NetworkLink } from "../types.ts";
import * as d3 from "d3";
// Configuration

8
src/lib/navigator/EventNetwork/utils/networkBuilder.ts

@ -6,10 +6,10 @@ @@ -6,10 +6,10 @@
*/
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types";
import type { NetworkNode, GraphData, GraphState } from "../types.ts";
import { nip19 } from "nostr-tools";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { getMatchingTags } from "$lib/utils/nostrUtils";
import { activeInboxRelays, activeOutboxRelays } from "../../../ndk.ts";
import { getMatchingTags } from "../../../utils/nostrUtils.ts";
import { get } from "svelte/store";
// Configuration
@ -20,7 +20,7 @@ const CONTENT_EVENT_KIND = 30041; @@ -20,7 +20,7 @@ const CONTENT_EVENT_KIND = 30041;
/**
* Debug logging function that only logs when DEBUG is true
*/
function debug(...args: any[]) {
function debug(...args: unknown[]) {
if (DEBUG) {
console.log("[NetworkBuilder]", ...args);
}

108
src/lib/ndk.ts

@ -8,29 +8,20 @@ import NDK, { @@ -8,29 +8,20 @@ import NDK, {
} from "@nostr-dev-kit/ndk";
import { get, writable, type Writable } from "svelte/store";
import {
secondaryRelays,
FeedType,
loginStorageKey,
communityRelays,
anonymousRelays,
searchRelays,
} from "./consts";
} from "./consts.ts";
import {
buildCompleteRelaySet,
testRelayConnection,
discoverLocalRelays,
getUserLocalRelays,
getUserBlockedRelays,
getUserOutboxRelays,
deduplicateRelayUrls,
} from "./utils/relay_management";
} from "./utils/relay_management.ts";
// Re-export testRelayConnection for components that need it
export { testRelayConnection };
import { startNetworkMonitoring, NetworkCondition } from "./utils/network_detection";
import { userStore } from "./stores/userStore";
import { userPubkey } from "$lib/stores/authStore.Svelte";
import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore";
import { userStore } from "./stores/userStore.ts";
import { userPubkey } from "./stores/authStore.Svelte.ts";
import { startNetworkStatusMonitoring, stopNetworkStatusMonitoring } from "./stores/networkStore.ts";
import { WebSocketPool } from "./data_structures/websocket_pool.ts";
export const ndkInstance: Writable<NDK> = writable();
export const ndkSignedIn = writable(false);
@ -59,7 +50,7 @@ class CustomRelayAuthPolicy { @@ -59,7 +50,7 @@ class CustomRelayAuthPolicy {
* @param relay The relay to authenticate with
* @returns Promise that resolves when authentication is complete
*/
async authenticate(relay: NDKRelay): Promise<void> {
authenticate(relay: NDKRelay): void {
if (!this.ndk.signer || !this.ndk.activeUser) {
console.warn(
"[NDK.ts] No signer or active user available for relay authentication",
@ -84,7 +75,7 @@ class CustomRelayAuthPolicy { @@ -84,7 +75,7 @@ class CustomRelayAuthPolicy {
relay.on("notice", (message: string) => {
if (message.includes("auth-required")) {
console.debug(`[NDK.ts] Auth required from ${relay.url}:`, message);
this.handleAuthRequired(relay, message);
this.handleAuthRequired(relay);
}
});
@ -94,7 +85,7 @@ class CustomRelayAuthPolicy { @@ -94,7 +85,7 @@ class CustomRelayAuthPolicy {
});
// Listen for authentication failures
relay.on("auth:failed", (error: any) => {
relay.on("auth:failed", (error) => {
console.error(
`[NDK.ts] Authentication failed for ${relay.url}:`,
error,
@ -151,10 +142,7 @@ class CustomRelayAuthPolicy { @@ -151,10 +142,7 @@ class CustomRelayAuthPolicy {
/**
* Handles auth-required error from relay
*/
private async handleAuthRequired(
relay: NDKRelay,
message: string,
): Promise<void> {
private async handleAuthRequired(relay: NDKRelay): Promise<void> {
const challenge = this.challenges.get(relay.url);
if (challenge) {
await this.handleAuthChallenge(relay, challenge);
@ -173,13 +161,13 @@ export function checkEnvironmentForWebSocketDowngrade(): void { @@ -173,13 +161,13 @@ export function checkEnvironmentForWebSocketDowngrade(): void {
console.debug("[NDK.ts] Environment Check for WebSocket Protocol:");
const isLocalhost =
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1";
const isHttp = window.location.protocol === "http:";
const isHttps = window.location.protocol === "https:";
globalThis.location.hostname === "localhost" ||
globalThis.location.hostname === "127.0.0.1";
const isHttp = globalThis.location.protocol === "http:";
const isHttps = globalThis.location.protocol === "https:";
console.debug("[NDK.ts] - Is localhost:", isLocalhost);
console.debug("[NDK.ts] - Protocol:", window.location.protocol);
console.debug("[NDK.ts] - Protocol:", globalThis.location.protocol);
console.debug("[NDK.ts] - Is HTTP:", isHttp);
console.debug("[NDK.ts] - Is HTTPS:", isHttps);
@ -205,23 +193,21 @@ export function checkEnvironmentForWebSocketDowngrade(): void { @@ -205,23 +193,21 @@ export function checkEnvironmentForWebSocketDowngrade(): void {
*/
export function checkWebSocketSupport(): void {
console.debug("[NDK.ts] WebSocket Support Diagnostics:");
console.debug("[NDK.ts] - Protocol:", window.location.protocol);
console.debug("[NDK.ts] - Hostname:", window.location.hostname);
console.debug("[NDK.ts] - Port:", window.location.port);
console.debug("[NDK.ts] - Protocol:", globalThis.location.protocol);
console.debug("[NDK.ts] - Hostname:", globalThis.location.hostname);
console.debug("[NDK.ts] - Port:", globalThis.location.port);
console.debug("[NDK.ts] - User Agent:", navigator.userAgent);
// Test if secure WebSocket is supported
try {
const testWs = new WebSocket("wss://echo.websocket.org");
testWs.onopen = () => {
WebSocketPool.instance.acquire("wss://echo.websocket.org").then((ws) => {
console.debug("[NDK.ts] ✓ Secure WebSocket (wss://) is supported");
testWs.close();
};
testWs.onerror = () => {
WebSocketPool.instance.release(ws);
}).catch((_) => {
console.warn("[NDK.ts] ✗ Secure WebSocket (wss://) may not be supported");
};
} catch (error) {
console.warn("[NDK.ts] ✗ WebSocket test failed:", error);
});
} catch {
console.warn("[NDK.ts] ✗ WebSocket test failed");
}
}
@ -266,46 +252,6 @@ function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string { @@ -266,46 +252,6 @@ function getRelayStorageKey(user: NDKUser, type: "inbox" | "outbox"): string {
return `${loginStorageKey}/${user.pubkey}/${type}`;
}
/**
* Stores the user's relay lists in local storage.
* @param user The user for whom to store the relay lists.
* @param inboxes The user's inbox relays.
* @param outboxes The user's outbox relays.
*/
function persistRelays(
user: NDKUser,
inboxes: Set<NDKRelay>,
outboxes: Set<NDKRelay>,
): void {
localStorage.setItem(
getRelayStorageKey(user, "inbox"),
JSON.stringify(Array.from(inboxes).map((relay) => relay.url)),
);
localStorage.setItem(
getRelayStorageKey(user, "outbox"),
JSON.stringify(Array.from(outboxes).map((relay) => relay.url)),
);
}
/**
* Retrieves the user's relay lists from local storage.
* @param user The user for whom to retrieve the relay lists.
* @returns A tuple of relay sets of the form `[inboxRelays, outboxRelays]`. Either set may be
* empty if no relay lists were stored for the user.
*/
function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] {
const inboxes = new Set<string>(
JSON.parse(localStorage.getItem(getRelayStorageKey(user, "inbox")) ?? "[]"),
);
const outboxes = new Set<string>(
JSON.parse(
localStorage.getItem(getRelayStorageKey(user, "outbox")) ?? "[]",
),
);
return [inboxes, outboxes];
}
export function clearPersistedRelays(user: NDKUser): void {
localStorage.removeItem(getRelayStorageKey(user, "inbox"));
localStorage.removeItem(getRelayStorageKey(user, "outbox"));
@ -468,7 +414,7 @@ export async function refreshRelayStoresOnNetworkChange(ndk: NDK): Promise<void> @@ -468,7 +414,7 @@ export async function refreshRelayStoresOnNetworkChange(ndk: NDK): Promise<void>
* Starts network monitoring for relay optimization
* @param ndk NDK instance
*/
export function startNetworkMonitoringForRelays(ndk: NDK): void {
export function startNetworkMonitoringForRelays(): void {
// Use centralized network monitoring instead of separate monitoring
startNetworkStatusMonitoring();
}
@ -529,7 +475,7 @@ export function initNdk(): NDK { @@ -529,7 +475,7 @@ export function initNdk(): NDK {
// Update relay stores after connection
await updateActiveRelayStores(ndk);
// Start network monitoring for relay optimization
startNetworkMonitoringForRelays(ndk);
startNetworkMonitoringForRelays();
} catch (error) {
console.warn("[NDK.ts] Failed to connect NDK:", error);
@ -543,7 +489,7 @@ export function initNdk(): NDK { @@ -543,7 +489,7 @@ export function initNdk(): NDK {
// Still try to update relay stores even if connection failed
try {
await updateActiveRelayStores(ndk);
startNetworkMonitoringForRelays(ndk);
startNetworkMonitoringForRelays();
} catch (storeError) {
console.warn("[NDK.ts] Failed to update relay stores:", storeError);
}

16
src/lib/parser.ts

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
// deno-lint-ignore-file no-this-alias
import NDK, { NDKEvent } from "@nostr-dev-kit/ndk";
import asciidoctor from "asciidoctor";
import Processor from "asciidoctor";
import type {
AbstractBlock,
AbstractNode,
Asciidoctor,
Block,
Document,
Extensions,
@ -13,7 +13,7 @@ import type { @@ -13,7 +13,7 @@ import type {
import he from "he";
import { writable, type Writable } from "svelte/store";
import { zettelKinds } from "./consts.ts";
import { getMatchingTags } from "$lib/utils/nostrUtils";
import { getMatchingTags } from "./utils/nostrUtils.ts";
interface IndexMetadata {
authors?: string[];
@ -65,7 +65,7 @@ export default class Pharos { @@ -65,7 +65,7 @@ export default class Pharos {
* hierarchically to form the Abstract Syntax Tree (AST) representation of the document.
*/
private asciidoctor: Asciidoctor;
private asciidoctor;
private pharosExtensions: Extensions.Registry;
@ -140,7 +140,7 @@ export default class Pharos { @@ -140,7 +140,7 @@ export default class Pharos {
// #region Public API
constructor(ndk: NDK) {
this.asciidoctor = asciidoctor();
this.asciidoctor = Processor();
this.pharosExtensions = this.asciidoctor.Extensions.create();
this.ndk = ndk;
@ -164,9 +164,9 @@ export default class Pharos { @@ -164,9 +164,9 @@ export default class Pharos {
private async loadAdvancedExtensions(): Promise<void> {
try {
const { createAdvancedExtensions } = await import(
"./utils/markup/asciidoctorExtensions"
"./utils/markup/asciidoctorExtensions.ts"
);
const advancedExtensions = createAdvancedExtensions();
createAdvancedExtensions();
// Note: Extensions merging might not be available in this version
// We'll handle this in the parse method instead
} catch (error) {
@ -549,7 +549,7 @@ export default class Pharos { @@ -549,7 +549,7 @@ export default class Pharos {
* - Each ID of a node containing children is mapped to the set of IDs of its children.
*/
private treeProcessor(
treeProcessor: Extensions.TreeProcessor,
_: Extensions.TreeProcessor,
document: Document,
) {
this.rootNodeId = this.generateNodeId(document);

10
src/lib/services/publisher.ts

@ -1,12 +1,8 @@ @@ -1,12 +1,8 @@
import { get } from "svelte/store";
import { ndkInstance } from "$lib/ndk";
import { getMimeTags } from "$lib/utils/mime";
import {
parseAsciiDocSections,
type ZettelSection,
} from "$lib/utils/ZettelParser";
import { ndkInstance } from "../ndk.ts";
import { getMimeTags } from "../utils/mime.ts";
import { parseAsciiDocSections } from "../utils/ZettelParser.ts";
import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
export interface PublishResult {
success: boolean;

2
src/lib/state.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { browser } from "$app/environment";
import { writable, type Writable } from "svelte/store";
import type { Tab } from "./types";
import type { Tab } from "./types.ts";
export const pathLoaded: Writable<boolean> = writable(false);

4
src/lib/stores.ts

@ -3,9 +3,9 @@ import { writable } from "svelte/store"; @@ -3,9 +3,9 @@ import { writable } from "svelte/store";
// The old feedType store is no longer needed since we use the new relay management system
// All relay selection is now handled by the activeInboxRelays and activeOutboxRelays stores in ndk.ts
export let idList = writable<string[]>([]);
export const idList = writable<string[]>([]);
export let alexandriaKinds = writable<number[]>([30040, 30041, 30818]);
export const alexandriaKinds = writable<number[]>([30040, 30041, 30818]);
export interface PublicationLayoutVisibility {
toc: boolean;

4
src/lib/stores/networkStore.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { writable, type Writable } from 'svelte/store';
import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '$lib/utils/network_detection';
import { writable } from "svelte/store";
import { detectNetworkCondition, NetworkCondition, startNetworkMonitoring } from '../utils/network_detection.ts';
// Network status store
export const networkCondition = writable<NetworkCondition>(NetworkCondition.ONLINE);

27
src/lib/stores/userStore.ts

@ -1,17 +1,17 @@ @@ -1,17 +1,17 @@
import { writable, get } from "svelte/store";
import type { NostrProfile } from "$lib/utils/nostrUtils";
import type { NostrProfile } from "../utils/nostrUtils.ts";
import type { NDKUser, NDKSigner } from "@nostr-dev-kit/ndk";
import {
import NDK, {
NDKNip07Signer,
NDKRelayAuthPolicies,
NDKRelaySet,
NDKRelay,
} from "@nostr-dev-kit/ndk";
import { getUserMetadata } from "$lib/utils/nostrUtils";
import { ndkInstance, activeInboxRelays, activeOutboxRelays, updateActiveRelayStores } from "$lib/ndk";
import { loginStorageKey } from "$lib/consts";
import { getUserMetadata } from "../utils/nostrUtils.ts";
import { ndkInstance, activeInboxRelays, activeOutboxRelays, updateActiveRelayStores } from "../ndk.ts";
import { loginStorageKey } from "../consts.ts";
import { nip19 } from "nostr-tools";
import { userPubkey } from "$lib/stores/authStore.Svelte";
import { userPubkey } from "../stores/authStore.Svelte.ts";
export interface UserState {
pubkey: string | null;
@ -69,7 +69,7 @@ function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] { @@ -69,7 +69,7 @@ function getPersistedRelays(user: NDKUser): [Set<string>, Set<string>] {
}
async function getUserPreferredRelays(
ndk: any,
ndk: NDK,
user: NDKUser,
fallbacks: readonly string[] = [...get(activeInboxRelays), ...get(activeOutboxRelays)],
): Promise<[Set<NDKRelay>, Set<NDKRelay>]> {
@ -90,9 +90,9 @@ async function getUserPreferredRelays( @@ -90,9 +90,9 @@ async function getUserPreferredRelays(
const outboxRelays = new Set<NDKRelay>();
if (relayList == null) {
const relayMap = await window.nostr?.getRelays?.();
const relayMap = await globalThis.nostr?.getRelays?.();
Object.entries(relayMap ?? {}).forEach(
([url, relayType]: [string, any]) => {
([url, relayType]: [string, Record<string, boolean | undefined>]) => {
const relay = new NDKRelay(
url,
NDKRelayAuthPolicies.signIn({ ndk }),
@ -139,15 +139,6 @@ function persistLogin(user: NDKUser, method: "extension" | "amber" | "npub") { @@ -139,15 +139,6 @@ function persistLogin(user: NDKUser, method: "extension" | "amber" | "npub") {
localStorage.setItem(loginMethodStorageKey, method);
}
function getPersistedLoginMethod(): "extension" | "amber" | "npub" | null {
return (
(localStorage.getItem(loginMethodStorageKey) as
| "extension"
| "amber"
| "npub") ?? null
);
}
function clearLogin() {
localStorage.removeItem(loginStorageKey);
localStorage.removeItem(loginMethodStorageKey);

2
src/lib/types.ts

@ -3,7 +3,7 @@ export type Tab = { @@ -3,7 +3,7 @@ export type Tab = {
type: TabType;
parent?: number;
previous?: Tab;
data?: any;
data?: unknown;
};
export type TabType =

8
src/lib/utils.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
import { getMatchingTags } from "./utils/nostrUtils";
import { getMatchingTags } from "./utils/nostrUtils.ts";
export function neventEncode(event: NDKEvent, relays: string[]) {
return nip19.neventEncode({
@ -97,8 +97,8 @@ export function isElementInViewport(el: string | HTMLElement) { @@ -97,8 +97,8 @@ export function isElementInViewport(el: string | HTMLElement) {
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
(globalThis.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (globalThis.innerWidth || document.documentElement.clientWidth)
);
}
@ -169,7 +169,7 @@ Array.prototype.findIndexAsync = function <T>( @@ -169,7 +169,7 @@ Array.prototype.findIndexAsync = function <T>(
* @param wait The number of milliseconds to delay
* @returns A debounced version of the function
*/
export function debounce<T extends (...args: any[]) => any>(
export function debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {

8
src/lib/utils/ZettelParser.ts

@ -1,9 +1,3 @@ @@ -1,9 +1,3 @@
import { ndkInstance } from "$lib/ndk";
import { signEvent, getEventHash } from "$lib/utils/nostrUtils";
import { getMimeTags } from "$lib/utils/mime";
import { communityRelays } from "$lib/consts";
import { nip19 } from "nostr-tools";
export interface ZettelSection {
title: string;
content: string;
@ -37,7 +31,7 @@ export function splitAsciiDocByHeadingLevel( @@ -37,7 +31,7 @@ export function splitAsciiDocByHeadingLevel(
export function parseZettelSection(section: string): ZettelSection {
const lines = section.split("\n");
let title = "Untitled";
let contentLines: string[] = [];
const contentLines: string[] = [];
let inHeader = true;
let tags: string[][] = [];
tags = extractTags(section);

35
src/lib/utils/community_checker.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { communityRelays } from "$lib/consts";
import { WebSocketPool } from "../data_structures/websocket_pool.ts";
import { RELAY_CONSTANTS, SEARCH_LIMITS } from "./search_constants";
// Cache for pubkeys with kind 1 events on communityRelay
@ -16,37 +17,31 @@ export async function checkCommunity(pubkey: string): Promise<boolean> { @@ -16,37 +17,31 @@ export async function checkCommunity(pubkey: string): Promise<boolean> {
// Try each community relay until we find one that works
for (const relayUrl of communityRelays) {
try {
const ws = new WebSocket(relayUrl);
const ws = await WebSocketPool.instance.acquire(relayUrl);
const result = await new Promise<boolean>((resolve) => {
ws.onopen = () => {
ws.send(
JSON.stringify([
"REQ",
RELAY_CONSTANTS.COMMUNITY_REQUEST_ID,
{
kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS,
authors: [pubkey],
limit: SEARCH_LIMITS.COMMUNITY_CHECK,
},
]),
);
};
ws.send(
JSON.stringify([
"REQ",
RELAY_CONSTANTS.COMMUNITY_REQUEST_ID,
{
kinds: RELAY_CONSTANTS.COMMUNITY_REQUEST_KINDS,
authors: [pubkey],
limit: SEARCH_LIMITS.COMMUNITY_CHECK,
},
]),
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data[0] === "EVENT" && data[2]?.kind === 1) {
communityCache.set(pubkey, true);
ws.close();
WebSocketPool.instance.release(ws);
resolve(true);
} else if (data[0] === "EOSE") {
communityCache.set(pubkey, false);
ws.close();
WebSocketPool.instance.release(ws);
resolve(false);
}
};
ws.onerror = () => {
ws.close();
resolve(false);
};
});
if (result) {

13
src/lib/utils/event_input_utils.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import type { NDKEvent } from "./nostrUtils";
import type { NDKEvent } from "./nostrUtils.ts";
import { get } from "svelte/store";
import { ndkInstance } from "$lib/ndk";
import { ndkInstance } from "../ndk.ts";
import { NDKEvent as NDKEventClass } from "@nostr-dev-kit/ndk";
import { EVENT_KINDS } from "./search_constants";
@ -149,13 +149,6 @@ function extractAsciiDocDocumentHeader(content: string): string | null { @@ -149,13 +149,6 @@ function extractAsciiDocDocumentHeader(content: string): string | null {
return match ? match[1].trim() : null;
}
/**
* Extracts all section headers (lines starting with '== ').
*/
function extractAsciiDocSectionHeaders(content: string): string[] {
return Array.from(content.matchAll(/^==\s+(.+)$/gm)).map((m) => m[1].trim());
}
/**
* Extracts the topmost Markdown # header (line starting with '# ').
*/
@ -181,7 +174,7 @@ function splitAsciiDocSections(content: string): { @@ -181,7 +174,7 @@ function splitAsciiDocSections(content: string): {
let current: string[] = [];
let foundFirstSection = false;
let hasPreamble = false;
let preambleContent: string[] = [];
const preambleContent: string[] = [];
for (const line of lines) {
// Skip document title lines (= header)

18
src/lib/utils/event_search.ts

@ -1,18 +1,18 @@ @@ -1,18 +1,18 @@
import { ndkInstance } from "$lib/ndk";
import { fetchEventWithFallback } from "$lib/utils/nostrUtils";
import { nip19 } from "$lib/utils/nostrUtils";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { ndkInstance } from "../ndk.ts";
import { fetchEventWithFallback } from "./nostrUtils.ts";
import { nip19 } from "nostr-tools";
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
import { get } from "svelte/store";
import { wellKnownUrl, isValidNip05Address } from "./search_utils";
import { TIMEOUTS, VALIDATION } from "./search_constants";
import { wellKnownUrl, isValidNip05Address } from "./search_utils.ts";
import { TIMEOUTS, VALIDATION } from "./search_constants.ts";
/**
* Search for a single event by ID or filter
*/
export async function searchEvent(query: string): Promise<NDKEvent | null> {
// Clean the query and normalize to lowercase
let cleanedQuery = query.replace(/^nostr:/, "").toLowerCase();
let filterOrId: any = cleanedQuery;
const cleanedQuery = query.replace(/^nostr:/, "").toLowerCase();
let filterOrId: NDKFilter | string = cleanedQuery;
// If it's a valid hex string, try as event id first, then as pubkey (profile)
if (
@ -164,7 +164,7 @@ export async function findContainingIndexEvents( @@ -164,7 +164,7 @@ export async function findContainingIndexEvents(
): Promise<NDKEvent[]> {
// Support all content event kinds that can be contained in indexes
const contentEventKinds = [30041, 30818, 30040, 30023];
if (!contentEventKinds.includes(contentEvent.kind)) {
if (!contentEventKinds.includes(contentEvent.kind!)) {
return [];
}

5
src/lib/utils/indexEventCache.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import type { NDKEvent } from "./nostrUtils";
import { CACHE_DURATIONS, TIMEOUTS } from "./search_constants";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { CACHE_DURATIONS, TIMEOUTS } from "./search_constants.ts";
export interface IndexEventCacheEntry {
events: NDKEvent[];
@ -84,7 +84,6 @@ class IndexEventCache { @@ -84,7 +84,6 @@ class IndexEventCache {
* Clear expired entries from cache
*/
cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (this.isExpired(entry)) {
this.cache.delete(key);

14
src/lib/utils/markup/advancedAsciidoctorPostProcessor.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { postProcessAsciidoctorHtml } from "./asciidoctorPostProcessor";
import { postProcessAsciidoctorHtml } from "./asciidoctorPostProcessor.ts";
import plantumlEncoder from "plantuml-encoder";
/**
@ -25,16 +25,16 @@ export async function postProcessAdvancedAsciidoctorHtml( @@ -25,16 +25,16 @@ export async function postProcessAdvancedAsciidoctorHtml(
processedHtml = processTikZBlocks(processedHtml);
// After all processing, apply highlight.js if available
if (
typeof window !== "undefined" &&
typeof window.hljs?.highlightAll === "function"
typeof globalThis !== "undefined" &&
typeof globalThis.hljs?.highlightAll === "function"
) {
setTimeout(() => window.hljs!.highlightAll(), 0);
setTimeout(() => globalThis.hljs!.highlightAll(), 0);
}
if (
typeof window !== "undefined" &&
typeof (window as any).MathJax?.typesetPromise === "function"
typeof globalThis !== "undefined" &&
typeof globalThis.MathJax?.typesetPromise === "function"
) {
setTimeout(() => (window as any).MathJax.typesetPromise(), 0);
setTimeout(() => globalThis.MathJax.typesetPromise(), 0);
}
return processedHtml;
} catch (error) {

24
src/lib/utils/markup/advancedMarkupParser.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { parseBasicmarkup } from "./basicMarkupParser";
import { parseBasicmarkup } from "./basicMarkupParser.ts";
import hljs from "highlight.js";
import "highlight.js/lib/common"; // Import common languages
import "highlight.js/styles/github-dark.css"; // Dark theme only
@ -35,14 +35,14 @@ const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm; @@ -35,14 +35,14 @@ const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm;
const CODE_BLOCK_REGEX = /^```(\w*)$/;
// LaTeX math regex patterns
const INLINE_MATH_REGEX = /\$([^$\n]+?)\$/g;
const DISPLAY_MATH_REGEX = /\$\$([\s\S]*?)\$\$/g;
const LATEX_BLOCK_REGEX = /\\\[([\s\S]*?)\\\]/g;
const LATEX_INLINE_REGEX = /\\\(([^)]+?)\\\)/g;
// const INLINE_MATH_REGEX = /\$([^$\n]+?)\$/g;
// const DISPLAY_MATH_REGEX = /\$\$([\s\S]*?)\$\$/g;
// const LATEX_BLOCK_REGEX = /\\\[([\s\S]*?)\\\]/g;
// const LATEX_INLINE_REGEX = /\\\(([^)]+?)\\\)/g;
// Add regex for LaTeX display math environments (e.g., \begin{pmatrix}...\end{pmatrix})
// Improved regex: match optional whitespace/linebreaks before and after, and allow for indented environments
const LATEX_ENV_BLOCK_REGEX =
/(?:^|\n)\s*\\begin\{([a-zA-Z*]+)\}([\s\S]*?)\\end\{\1\}\s*(?=\n|$)/gm;
// const LATEX_ENV_BLOCK_REGEX =
// /(?:^|\n)\s*\\begin\{([a-zA-Z*]+)\}([\s\S]*?)\\end\{\1\}\s*(?=\n|$)/gm;
/**
* Process headings (both styles)
@ -290,7 +290,7 @@ function processCodeBlocks(text: string): { @@ -290,7 +290,7 @@ function processCodeBlocks(text: string): {
if (currentLanguage.toLowerCase() === "json") {
try {
formattedCode = JSON.stringify(JSON.parse(code), null, 2);
} catch (e: unknown) {
} catch {
formattedCode = code;
}
}
@ -333,7 +333,7 @@ function processCodeBlocks(text: string): { @@ -333,7 +333,7 @@ function processCodeBlocks(text: string): {
if (currentLanguage.toLowerCase() === "json") {
try {
formattedCode = JSON.stringify(JSON.parse(code), null, 2);
} catch (e: unknown) {
} catch {
formattedCode = code;
}
}
@ -402,7 +402,7 @@ function restoreCodeBlocks(text: string, blocks: Map<string, string>): string { @@ -402,7 +402,7 @@ function restoreCodeBlocks(text: string, blocks: Map<string, string>): string {
*/
function processDollarMath(content: string): string {
// Display math: $$...$$ (multi-line, not empty)
content = content.replace(/\$\$([\s\S]*?\S[\s\S]*?)\$\$/g, (match, expr) => {
content = content.replace(/\$\$([\s\S]*?\S[\s\S]*?)\$\$/g, (_match, expr) => {
if (isLaTeXContent(expr)) {
return `<div class="math-block">$$${expr}$$</div>`;
} else {
@ -412,7 +412,7 @@ function processDollarMath(content: string): string { @@ -412,7 +412,7 @@ function processDollarMath(content: string): string {
}
});
// Inline math: $...$ (not empty, not just whitespace)
content = content.replace(/\$([^\s$][^$\n]*?)\$/g, (match, expr) => {
content = content.replace(/\$([^\s$][^$\n]*?)\$/g, (_match, expr) => {
if (isLaTeXContent(expr)) {
return `<span class="math-inline">$${expr}$</span>`;
} else {
@ -428,7 +428,7 @@ function processDollarMath(content: string): string { @@ -428,7 +428,7 @@ function processDollarMath(content: string): string {
*/
function processMathExpressions(content: string): string {
// Only process LaTeX within inline code blocks (backticks)
return content.replace(INLINE_CODE_REGEX, (match, code) => {
return content.replace(INLINE_CODE_REGEX, (_match, code) => {
const trimmedCode = code.trim();
// Check for unsupported LaTeX environments (like tabular) first

25
src/lib/utils/markup/asciidoctorExtensions.ts

@ -1,17 +1,6 @@ @@ -1,17 +1,6 @@
import { renderTikZ } from "./tikzRenderer";
import asciidoctor from "asciidoctor";
// Simple math rendering using MathJax CDN
function renderMath(content: string): string {
return `<div class="math-block" data-math="${encodeURIComponent(content)}">
<div class="math-content">${content}</div>
<script>
if (typeof MathJax !== 'undefined') {
MathJax.typesetPromise([document.querySelector('.math-content')]);
}
</script>
</div>`;
}
// deno-lint-ignore-file no-this-alias no-explicit-any
import Processor from "asciidoctor";
import { renderTikZ } from "./tikzRenderer.ts";
// Simple PlantUML rendering using PlantUML server
function renderPlantUML(content: string): string {
@ -27,7 +16,7 @@ function renderPlantUML(content: string): string { @@ -27,7 +16,7 @@ function renderPlantUML(content: string): string {
* including Asciimath/Latex, PlantUML, BPMN, and TikZ
*/
export function createAdvancedExtensions(): any {
const Asciidoctor = asciidoctor();
const Asciidoctor = Processor();
const extensions = Asciidoctor.Extensions.create();
// Math rendering extension (Asciimath/Latex)
@ -95,7 +84,7 @@ export function createAdvancedExtensions(): any { @@ -95,7 +84,7 @@ export function createAdvancedExtensions(): any {
/**
* Processes math blocks (stem blocks) and converts them to rendered HTML
*/
function processMathBlocks(treeProcessor: any, document: any): void {
function processMathBlocks(_: any, document: any): void {
const blocks = document.getBlocks();
for (const block of blocks) {
if (block.getContext() === "stem") {
@ -131,7 +120,7 @@ function processMathBlocks(treeProcessor: any, document: any): void { @@ -131,7 +120,7 @@ function processMathBlocks(treeProcessor: any, document: any): void {
/**
* Processes PlantUML blocks and converts them to rendered SVG
*/
function processPlantUMLBlocks(treeProcessor: any, document: any): void {
function processPlantUMLBlocks(_: any, document: any): void {
const blocks = document.getBlocks();
for (const block of blocks) {
@ -156,7 +145,7 @@ function processPlantUMLBlocks(treeProcessor: any, document: any): void { @@ -156,7 +145,7 @@ function processPlantUMLBlocks(treeProcessor: any, document: any): void {
/**
* Processes TikZ blocks and converts them to rendered SVG
*/
function processTikZBlocks(treeProcessor: any, document: any): void {
function processTikZBlocks(_: any, document: any): void {
const blocks = document.getBlocks();
for (const block of blocks) {

8
src/lib/utils/markup/basicMarkupParser.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { processNostrIdentifiers } from "../nostrUtils";
import { processNostrIdentifiers } from "../nostrUtils.ts";
import * as emoji from "node-emoji";
import { nip19 } from "nostr-tools";
@ -236,7 +236,7 @@ function processBasicFormatting(content: string): string { @@ -236,7 +236,7 @@ function processBasicFormatting(content: string): string {
processedText = replaceAlexandriaNostrLinks(processedText);
// Process markup images first
processedText = processedText.replace(MARKUP_IMAGE, (match, alt, url) => {
processedText = processedText.replace(MARKUP_IMAGE, (_match, alt, url) => {
url = stripTrackingParams(url);
if (YOUTUBE_URL_REGEX.test(url)) {
const videoId = extractYouTubeVideoId(url);
@ -261,7 +261,7 @@ function processBasicFormatting(content: string): string { @@ -261,7 +261,7 @@ function processBasicFormatting(content: string): string {
// Process markup links
processedText = processedText.replace(
MARKUP_LINK,
(match, text, url) =>
(_match, text, url) =>
`<a href="${stripTrackingParams(url)}" class="text-primary-600 dark:text-primary-500 hover:underline" target="_blank" rel="noopener noreferrer">${text}</a>`,
);
@ -303,7 +303,7 @@ function processBasicFormatting(content: string): string { @@ -303,7 +303,7 @@ function processBasicFormatting(content: string): string {
});
processedText = processedText.replace(
STRIKETHROUGH_REGEX,
(match, doubleText, singleText) => {
(_match, doubleText, singleText) => {
const text = doubleText || singleText;
return `<del class="line-through">${text}</del>`;
},

11
src/lib/utils/network_detection.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { deduplicateRelayUrls } from './relay_management';
import { deduplicateRelayUrls } from "./relay_management.ts";
/**
* Network conditions for relay selection
@ -26,7 +26,7 @@ export async function isNetworkOnline(): Promise<boolean> { @@ -26,7 +26,7 @@ export async function isNetworkOnline(): Promise<boolean> {
for (const endpoint of NETWORK_ENDPOINTS) {
try {
// Use a simple fetch without HEAD method to avoid CORS issues
const response = await fetch(endpoint, {
await fetch(endpoint, {
method: 'GET',
cache: 'no-cache',
signal: AbortSignal.timeout(3000),
@ -127,8 +127,7 @@ export function getRelaySetForNetworkCondition( @@ -127,8 +127,7 @@ export function getRelaySetForNetworkCondition(
outboxRelays: []
};
}
case NetworkCondition.SLOW:
case NetworkCondition.SLOW: {
// Local relays + low bandwidth relays when slow (deduplicated)
console.debug('[network_detection.ts] Using local + low bandwidth relays (slow network)');
const slowInboxRelays = deduplicateRelayUrls([...discoveredLocalRelays, ...lowbandwidthRelays]);
@ -137,7 +136,7 @@ export function getRelaySetForNetworkCondition( @@ -137,7 +136,7 @@ export function getRelaySetForNetworkCondition(
inboxRelays: slowInboxRelays,
outboxRelays: slowOutboxRelays
};
}
case NetworkCondition.ONLINE:
default:
// Full relay set when online
@ -177,7 +176,7 @@ export function startNetworkMonitoring( @@ -177,7 +176,7 @@ export function startNetworkMonitoring(
checkNetwork();
// Set up periodic monitoring
intervalId = window.setInterval(checkNetwork, checkInterval);
intervalId = globalThis.setInterval(checkNetwork, checkInterval);
// Return function to stop monitoring
return () => {

21
src/lib/utils/nostrEventService.ts

@ -1,10 +1,9 @@ @@ -1,10 +1,9 @@
import { nip19 } from "nostr-tools";
import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils";
import { getEventHash, signEvent, prefixNostrAddresses } from "./nostrUtils.ts";
import { get } from "svelte/store";
import { goto } from "$app/navigation";
import { EVENT_KINDS, TIME_CONSTANTS, TIMEOUTS } from "./search_constants";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { ndkInstance } from "$lib/ndk";
import { EVENT_KINDS, TIME_CONSTANTS } from "./search_constants.ts";
import { ndkInstance } from "../ndk.ts";
import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
export interface RootEventInfo {
@ -139,7 +138,6 @@ export function extractParentEventInfo(parent: NDKEvent): ParentEventInfo { @@ -139,7 +138,6 @@ export function extractParentEventInfo(parent: NDKEvent): ParentEventInfo {
*/
function buildRootScopeTags(
rootInfo: RootEventInfo,
parentInfo: ParentEventInfo,
): string[][] {
const tags: string[][] = [];
@ -292,7 +290,7 @@ export function buildReplyTags( @@ -292,7 +290,7 @@ export function buildReplyTags(
// For regular events, use E/e tags
if (isReplyToComment) {
// Reply to a comment - distinguish root from parent
addTags(tags, ...buildRootScopeTags(rootInfo, parentInfo));
addTags(tags, ...buildRootScopeTags(rootInfo));
addTags(
tags,
createTag("e", parent.id, parentInfo.parentRelay),
@ -301,7 +299,7 @@ export function buildReplyTags( @@ -301,7 +299,7 @@ export function buildReplyTags(
);
} else {
// Top-level comment or regular event
addTags(tags, ...buildRootScopeTags(rootInfo, parentInfo));
addTags(tags, ...buildRootScopeTags(rootInfo));
addTags(tags, ...buildParentScopeTags(parent, parentInfo, rootInfo));
}
}
@ -318,6 +316,7 @@ export async function createSignedEvent( @@ -318,6 +316,7 @@ export async function createSignedEvent(
pubkey: string,
kind: number,
tags: string[][],
// deno-lint-ignore no-explicit-any
): Promise<{ id: string; sig: string; event: any }> {
const prefixedContent = prefixNostrAddresses(content);
@ -337,8 +336,8 @@ export async function createSignedEvent( @@ -337,8 +336,8 @@ export async function createSignedEvent(
};
let sig, id;
if (typeof window !== "undefined" && window.nostr && window.nostr.signEvent) {
const signed = await window.nostr.signEvent(eventToSign);
if (typeof window !== "undefined" && globalThis.nostr && globalThis.nostr.signEvent) {
const signed = await globalThis.nostr.signEvent(eventToSign);
sig = signed.sig as string;
id = "id" in signed ? (signed.id as string) : getEventHash(eventToSign);
} else {
@ -364,7 +363,7 @@ export async function createSignedEvent( @@ -364,7 +363,7 @@ export async function createSignedEvent(
* @returns Promise that resolves to array of successful relay URLs
*/
export async function publishEvent(
event: NDKEvent | any,
event: NDKEvent,
relayUrls: string[],
): Promise<string[]> {
const successfulRelays: string[] = [];
@ -427,6 +426,7 @@ export function navigateToEvent(eventId: string): void { @@ -427,6 +426,7 @@ export function navigateToEvent(eventId: string): void {
}
// Helper functions to ensure relay and pubkey are always strings
// deno-lint-ignore no-explicit-any
function getRelayString(relay: any): string {
if (!relay) return "";
if (typeof relay === "string") return relay;
@ -434,6 +434,7 @@ function getRelayString(relay: any): string { @@ -434,6 +434,7 @@ function getRelayString(relay: any): string {
return "";
}
// deno-lint-ignore no-explicit-any
function getPubkeyString(pubkey: any): string {
if (!pubkey) return "";
if (typeof pubkey === "string") return pubkey;

26
src/lib/utils/nostrUtils.ts

@ -1,17 +1,17 @@ @@ -1,17 +1,17 @@
import { get } from "svelte/store";
import { nip19 } from "nostr-tools";
import { ndkInstance } from "$lib/ndk";
import { npubCache } from "./npubCache";
import { ndkInstance } from "../ndk.ts";
import { npubCache } from "./npubCache.ts";
import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk";
import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk";
import { communityRelays, secondaryRelays, anonymousRelays } from "$lib/consts";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import type { NDKFilter, NDKKind, NostrEvent } from "@nostr-dev-kit/ndk";
import { communityRelays, secondaryRelays } from "../consts.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk";
import { sha256 } from "@noble/hashes/sha256";
import { sha256 } from "@noble/hashes/sha2.js";
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/hashes/utils";
import { wellKnownUrl } from "./search_utility";
import { TIMEOUTS, VALIDATION } from "./search_constants";
import { wellKnownUrl } from "./search_utility.ts";
import { VALIDATION } from "./search_constants.ts";
const badgeCheckSvg =
'<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2c-.791 0-1.55.314-2.11.874l-.893.893a.985.985 0 0 1-.696.288H7.04A2.984 2.984 0 0 0 4.055 7.04v1.262a.986.986 0 0 1-.288.696l-.893.893a2.984 2.984 0 0 0 0 4.22l.893.893a.985.985 0 0 1 .288.696v1.262a2.984 2.984 0 0 0 2.984 2.984h1.262c.261 0 .512.104.696.288l.893.893a2.984 2.984 0 0 0 4.22 0l.893-.893a.985.985 0 0 1 .696-.288h1.262a2.984 2.984 0 0 0 2.984-2.984V15.7c0-.261.104-.512.288-.696l.893-.893a2.984 2.984 0 0 0 0-4.22l-.893-.893a.985.985 0 0 1-.288-.696V7.04a2.984 2.984 0 0 0-2.984-2.984h-1.262a.985.985 0 0 1-.696-.288l-.893-.893A2.984 2.984 0 0 0 12 2Zm3.683 7.73a1 1 0 1 0-1.414-1.413l-4.253 4.253-1.277-1.277a1 1 0 0 0-1.415 1.414l1.985 1.984a1 1 0 0 0 1.414 0l4.96-4.96Z" clip-rule="evenodd"/></svg>';
@ -442,7 +442,7 @@ export async function fetchEventWithFallback( @@ -442,7 +442,7 @@ export async function fetchEventWithFallback(
// Use both inbox and outbox relays for better event discovery
const inboxRelays = get(activeInboxRelays);
const outboxRelays = get(activeOutboxRelays);
const allRelays = [...(inboxRelays || []), ...(outboxRelays || [])];
const allRelays = [...inboxRelays, ...outboxRelays];
console.log("fetchEventWithFallback: Using inbox relays:", inboxRelays);
console.log("fetchEventWithFallback: Using outbox relays:", outboxRelays);
@ -464,7 +464,7 @@ export async function fetchEventWithFallback( @@ -464,7 +464,7 @@ export async function fetchEventWithFallback(
console.log("fetchEventWithFallback: Relay set size:", relaySet.relays.size);
console.log("fetchEventWithFallback: Filter:", filterOrId);
console.log("fetchEventWithFallback: Relay URLs:", Array.from(relaySet.relays).map((r: any) => r.url));
console.log("fetchEventWithFallback: Relay URLs:", Array.from(relaySet.relays).map((r) => r.url));
let found: NDKEvent | null = null;
@ -488,7 +488,7 @@ export async function fetchEventWithFallback( @@ -488,7 +488,7 @@ export async function fetchEventWithFallback(
if (!found) {
const timeoutSeconds = timeoutMs / 1000;
const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", ");
const relayUrls = Array.from(relaySet.relays).map((r) => r.url).join(", ");
console.warn(
`fetchEventWithFallback: Event not found after ${timeoutSeconds}s timeout. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`,
);
@ -501,7 +501,7 @@ export async function fetchEventWithFallback( @@ -501,7 +501,7 @@ export async function fetchEventWithFallback(
} catch (err) {
if (err instanceof Error && err.message === 'Timeout') {
const timeoutSeconds = timeoutMs / 1000;
const relayUrls = Array.from(relaySet.relays).map((r: any) => r.url).join(", ");
const relayUrls = Array.from(relaySet.relays).map((r) => r.url).join(", ");
console.warn(
`fetchEventWithFallback: Event fetch timed out after ${timeoutSeconds}s. Tried inbox relays: ${relayUrls}. Some relays may be offline or slow.`,
);
@ -536,7 +536,7 @@ export function createRelaySetFromUrls(relayUrls: string[], ndk: NDK) { @@ -536,7 +536,7 @@ export function createRelaySetFromUrls(relayUrls: string[], ndk: NDK) {
return NDKRelaySetFromNDK.fromRelayUrls(relayUrls, ndk);
}
export function createNDKEvent(ndk: NDK, rawEvent: any) {
export function createNDKEvent(ndk: NDK, rawEvent: NDKEvent | NostrEvent | undefined) {
return new NDKEvent(ndk, rawEvent);
}

30
src/lib/utils/profile_search.ts

@ -1,19 +1,16 @@ @@ -1,19 +1,16 @@
import { ndkInstance } from "$lib/ndk";
import { getUserMetadata, getNpubFromNip05 } from "$lib/utils/nostrUtils";
import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
import { searchCache } from "$lib/utils/searchCache";
import { communityRelays, secondaryRelays } from "$lib/consts";
import { ndkInstance } from "../ndk.ts";
import { getUserMetadata, getNpubFromNip05 } from "./nostrUtils.ts";
import NDK, { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
import { searchCache } from "./searchCache.ts";
import { communityRelays, secondaryRelays } from "../consts.ts";
import { get } from "svelte/store";
import type { NostrProfile, ProfileSearchResult } from "./search_types";
import type { NostrProfile, ProfileSearchResult } from "./search_types.ts";
import {
fieldMatches,
nip05Matches,
normalizeSearchTerm,
COMMON_DOMAINS,
createProfileFromEvent,
} from "./search_utils";
import { checkCommunityStatus } from "./community_checker";
import { TIMEOUTS } from "./search_constants";
} from "./search_utils.ts";
/**
* Search for profiles by various criteria (display name, name, NIP-05, npub)
@ -92,7 +89,7 @@ export async function searchProfiles( @@ -92,7 +89,7 @@ export async function searchProfiles(
} else {
// Try NIP-05 search first (faster than relay search)
console.log("Starting NIP-05 search for:", normalizedSearchTerm);
foundProfiles = await searchNip05Domains(normalizedSearchTerm, ndk);
foundProfiles = await searchNip05Domains(normalizedSearchTerm);
console.log(
"NIP-05 search completed, found:",
foundProfiles.length,
@ -145,7 +142,6 @@ export async function searchProfiles( @@ -145,7 +142,6 @@ export async function searchProfiles(
*/
async function searchNip05Domains(
searchTerm: string,
ndk: any,
): Promise<NostrProfile[]> {
const foundProfiles: NostrProfile[] = [];
@ -260,10 +256,9 @@ async function searchNip05Domains( @@ -260,10 +256,9 @@ async function searchNip05Domains(
*/
async function quickRelaySearch(
searchTerm: string,
ndk: any,
ndk: NDK,
): Promise<NostrProfile[]> {
console.log("quickRelaySearch called with:", searchTerm);
const foundProfiles: NostrProfile[] = [];
// Normalize the search term for relay search
const normalizedSearchTerm = normalizeSearchTerm(searchTerm);
@ -286,7 +281,7 @@ async function quickRelaySearch( @@ -286,7 +281,7 @@ async function quickRelaySearch(
.filter(Boolean);
// Search all relays in parallel with short timeout
const searchPromises = relaySets.map(async (relaySet, index) => {
const searchPromises = relaySets.map((relaySet, index) => {
if (!relaySet) return [];
return new Promise<NostrProfile[]>((resolve) => {
@ -299,7 +294,8 @@ async function quickRelaySearch( @@ -299,7 +294,8 @@ async function quickRelaySearch(
const sub = ndk.subscribe(
{ kinds: [0] },
{ closeOnEose: true, relaySet },
{ closeOnEose: true },
relaySet,
);
sub.on("event", (event: NDKEvent) => {
@ -351,7 +347,7 @@ async function quickRelaySearch( @@ -351,7 +347,7 @@ async function quickRelaySearch(
foundInRelay.push(profile);
}
}
} catch (e) {
} catch {
// Invalid JSON or other error, skip
}
});

73
src/lib/utils/relayDiagnostics.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import NDK from "@nostr-dev-kit/ndk";
import { TIMEOUTS } from "./search_constants";
import { WebSocketPool } from "../data_structures/websocket_pool.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { TIMEOUTS } from "./search_constants.ts";
import { get } from "svelte/store";
export interface RelayDiagnostic {
@ -16,70 +16,35 @@ export interface RelayDiagnostic { @@ -16,70 +16,35 @@ export interface RelayDiagnostic {
*/
export async function testRelay(url: string): Promise<RelayDiagnostic> {
const startTime = Date.now();
const ws = await WebSocketPool.instance.acquire(url);
return new Promise((resolve) => {
const ws = new WebSocket(url);
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
ws.close();
resolve({
url,
connected: false,
requiresAuth: false,
error: "Connection timeout",
responseTime: Date.now() - startTime,
});
}
WebSocketPool.instance.release(ws);
resolve({
url,
connected: false,
requiresAuth: false,
error: "Connection timeout",
responseTime: Date.now() - startTime,
});
}, TIMEOUTS.RELAY_DIAGNOSTICS);
ws.onopen = () => {
if (!resolved) {
resolved = true;
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data[0] === "NOTICE" && data[1]?.includes("auth-required")) {
clearTimeout(timeout);
ws.close();
WebSocketPool.instance.release(ws);
resolve({
url,
connected: true,
requiresAuth: false,
requiresAuth: true,
responseTime: Date.now() - startTime,
});
}
};
ws.onerror = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
resolve({
url,
connected: false,
requiresAuth: false,
error: "WebSocket error",
responseTime: Date.now() - startTime,
});
}
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data[0] === "NOTICE" && data[1]?.includes("auth-required")) {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
ws.close();
resolve({
url,
connected: true,
requiresAuth: true,
responseTime: Date.now() - startTime,
});
}
}
};
}
});
}
/**

34
src/lib/utils/relay_management.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import NDK, { NDKRelay, NDKUser } from "@nostr-dev-kit/ndk";
import { communityRelays, searchRelays, secondaryRelays, anonymousRelays, lowbandwidthRelays, localRelays } from "../consts";
import { getRelaySetForNetworkCondition, NetworkCondition } from "./network_detection";
import { networkCondition } from "../stores/networkStore";
import NDK, { NDKKind, NDKRelay, NDKUser } from "@nostr-dev-kit/ndk";
import { searchRelays, secondaryRelays, anonymousRelays, lowbandwidthRelays, localRelays } from "../consts.ts";
import { getRelaySetForNetworkCondition } from "./network_detection.ts";
import { networkCondition } from "../stores/networkStore.ts";
import { get } from "svelte/store";
/**
@ -48,7 +48,7 @@ export function deduplicateRelayUrls(urls: string[]): string[] { @@ -48,7 +48,7 @@ export function deduplicateRelayUrls(urls: string[]): string[] {
* @param ndk The NDK instance
* @returns Promise that resolves to connection status
*/
export async function testRelayConnection(
export function testRelayConnection(
relayUrl: string,
ndk: NDK,
): Promise<{
@ -163,7 +163,7 @@ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<stri @@ -163,7 +163,7 @@ async function testLocalRelays(localRelayUrls: string[], ndk: NDK): Promise<stri
} else {
console.debug(`[relay_management.ts] Local relay failed: ${url} - ${result.error}`);
}
} catch (error) {
} catch {
// Silently ignore local relay failures - they're optional
console.debug(`[relay_management.ts] Local relay error (ignored): ${url}`);
}
@ -188,7 +188,7 @@ export async function discoverLocalRelays(ndk: NDK): Promise<string[]> { @@ -188,7 +188,7 @@ export async function discoverLocalRelays(ndk: NDK): Promise<string[]> {
}
// Convert wss:// URLs from consts to ws:// for local testing
const localRelayUrls = localRelays.map(url =>
const localRelayUrls = localRelays.map((url: string) =>
url.replace(/^wss:\/\//, 'ws://')
);
@ -197,7 +197,7 @@ export async function discoverLocalRelays(ndk: NDK): Promise<string[]> { @@ -197,7 +197,7 @@ export async function discoverLocalRelays(ndk: NDK): Promise<string[]> {
// If no local relays are working, return empty array
// The network detection logic will provide fallback relays
return workingRelays;
} catch (error) {
} catch {
// Silently fail and return empty array
return [];
}
@ -213,7 +213,7 @@ export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise<strin @@ -213,7 +213,7 @@ export async function getUserLocalRelays(ndk: NDK, user: NDKUser): Promise<strin
try {
const localRelayEvent = await ndk.fetchEvent(
{
kinds: [10432 as any],
kinds: [10432 as NDKKind],
authors: [user.pubkey],
},
{
@ -338,8 +338,8 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise<stri @@ -338,8 +338,8 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise<stri
export async function getExtensionRelays(): Promise<string[]> {
try {
// Check if we're in a browser environment with extension support
if (typeof window === 'undefined' || !window.nostr) {
console.debug('[relay_management.ts] No window.nostr available');
if (typeof window === 'undefined' || !globalThis.nostr) {
console.debug('[relay_management.ts] No globalThis.nostr available');
return [];
}
@ -348,10 +348,10 @@ export async function getExtensionRelays(): Promise<string[]> { @@ -348,10 +348,10 @@ export async function getExtensionRelays(): Promise<string[]> {
// Try to get relays from the extension's API
// Different extensions may expose their relay config differently
if (window.nostr.getRelays) {
if (globalThis.nostr.getRelays) {
console.debug('[relay_management.ts] getRelays() method found, calling it...');
try {
const relays = await window.nostr.getRelays();
const relays = await globalThis.nostr.getRelays();
console.debug('[relay_management.ts] getRelays() returned:', relays);
if (relays && typeof relays === 'object') {
// Convert relay object to array of URLs
@ -363,7 +363,7 @@ export async function getExtensionRelays(): Promise<string[]> { @@ -363,7 +363,7 @@ export async function getExtensionRelays(): Promise<string[]> {
console.debug('[relay_management.ts] Extension getRelays() failed:', error);
}
} else {
console.debug('[relay_management.ts] getRelays() method not found on window.nostr');
console.debug('[relay_management.ts] getRelays() method not found on globalThis.nostr');
}
// If getRelays() didn't work, try alternative methods
@ -457,7 +457,7 @@ export async function buildCompleteRelaySet( @@ -457,7 +457,7 @@ export async function buildCompleteRelaySet(
try {
blockedRelays = await getUserBlockedRelays(ndk, user);
console.debug('[relay_management.ts] buildCompleteRelaySet: User blocked relays:', blockedRelays);
} catch (error) {
} catch {
// Silently ignore blocked relay fetch errors
}
@ -511,8 +511,8 @@ export async function buildCompleteRelaySet( @@ -511,8 +511,8 @@ export async function buildCompleteRelaySet(
// Filter out blocked relays and deduplicate final sets
const finalRelaySet = {
inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter(r => !blockedRelays.includes(r))),
outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter(r => !blockedRelays.includes(r)))
inboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.inboxRelays.filter((r: string) => !blockedRelays.includes(r))),
outboxRelays: deduplicateRelayUrls(networkOptimizedRelaySet.outboxRelays.filter((r: string) => !blockedRelays.includes(r)))
};
// If no relays are working, use anonymous relays as fallback

5
src/lib/utils/searchCache.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import type { NDKEvent } from "./nostrUtils";
import { CACHE_DURATIONS, TIMEOUTS } from "./search_constants";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { CACHE_DURATIONS, TIMEOUTS } from "./search_constants.ts";
export interface SearchResult {
events: NDKEvent[];
@ -78,7 +78,6 @@ class SearchCache { @@ -78,7 +78,6 @@ class SearchCache {
* Clear expired entries from cache
*/
cleanup(): void {
const now = Date.now();
for (const [key, result] of this.cache.entries()) {
if (this.isExpired(result)) {
this.cache.delete(key);

6
src/lib/utils/search_types.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { NDKEvent, NDKFilter, NDKSubscription } from "@nostr-dev-kit/ndk";
/**
* Extended NostrProfile interface for search results
@ -45,7 +45,7 @@ export type SearchSubscriptionType = "d" | "t" | "n"; @@ -45,7 +45,7 @@ export type SearchSubscriptionType = "d" | "t" | "n";
* Search filter configuration
*/
export interface SearchFilter {
filter: any;
filter: NDKFilter;
subscriptionType: string;
}
@ -65,5 +65,5 @@ export interface SecondOrderSearchParams { @@ -65,5 +65,5 @@ export interface SecondOrderSearchParams {
*/
export interface SearchCallbacks {
onSecondOrderUpdate?: (result: SearchResult) => void;
onSubscriptionCreated?: (sub: any) => void;
onSubscriptionCreated?: (sub: NDKSubscription) => void;
}

7
src/lib/utils/search_utils.ts

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
import { NDKEvent } from "@nostr-dev-kit/ndk";
/**
* Generate well-known NIP-05 URL
*/
@ -85,14 +87,15 @@ export const COMMON_DOMAINS = [ @@ -85,14 +87,15 @@ export const COMMON_DOMAINS = [
/**
* Check if an event is an emoji reaction (kind 7)
*/
export function isEmojiReaction(event: any): boolean {
export function isEmojiReaction(event: NDKEvent): boolean {
return event.kind === 7;
}
/**
* Create a profile object from event data
*/
export function createProfileFromEvent(event: any, profileData: any): any {
// deno-lint-ignore no-explicit-any
export function createProfileFromEvent(event: NDKEvent, profileData: any): any {
return {
name: profileData.name,
displayName: profileData.displayName || profileData.display_name,

46
src/lib/utils/subscription_search.ts

@ -1,26 +1,25 @@ @@ -1,26 +1,25 @@
import { ndkInstance } from "$lib/ndk";
import { getMatchingTags, getNpubFromNip05 } from "$lib/utils/nostrUtils";
import { nip19 } from "$lib/utils/nostrUtils";
// deno-lint-ignore-file no-explicit-any
import { ndkInstance } from "../ndk.ts";
import { getMatchingTags, getNpubFromNip05 } from "./nostrUtils.ts";
import { nip19 } from "./nostrUtils.ts";
import { NDKRelaySet, NDKEvent } from "@nostr-dev-kit/ndk";
import { searchCache } from "$lib/utils/searchCache";
import { communityRelays, searchRelays } from "$lib/consts";
import { searchCache } from "./searchCache.ts";
import { communityRelays, searchRelays } from "../consts.ts";
import { get } from "svelte/store";
import type {
SearchResult,
SearchSubscriptionType,
SearchFilter,
SearchCallbacks,
SecondOrderSearchParams,
} from "./search_types";
} from "./search_types.ts";
import {
fieldMatches,
nip05Matches,
normalizeSearchTerm,
COMMON_DOMAINS,
isEmojiReaction,
} from "./search_utils";
import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
} from "./search_utils.ts";
import { TIMEOUTS, SEARCH_LIMITS } from "./search_constants.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
// Helper function to normalize URLs for comparison
const normalizeUrl = (url: string): string => {
@ -132,7 +131,6 @@ export async function searchBySubscription( @@ -132,7 +131,6 @@ export async function searchBySubscription(
searchFilter,
searchState,
callbacks,
abortSignal,
cleanup,
);
@ -160,7 +158,6 @@ export async function searchBySubscription( @@ -160,7 +158,6 @@ export async function searchBySubscription(
searchFilter,
searchState,
callbacks,
abortSignal,
cleanup,
);
}
@ -215,26 +212,30 @@ async function createSearchFilter( @@ -215,26 +212,30 @@ async function createSearchFilter(
});
switch (searchType) {
case "d":
case "d": {
const dFilter = {
filter: { "#d": [normalizedSearchTerm] },
subscriptionType: "d-tag",
};
console.log("subscription_search: Created d-tag filter:", dFilter);
return dFilter;
case "t":
}
case "t": {
const tFilter = {
filter: { "#t": [normalizedSearchTerm] },
subscriptionType: "t-tag",
};
console.log("subscription_search: Created t-tag filter:", tFilter);
return tFilter;
case "n":
}
case "n": {
const nFilter = await createProfileSearchFilter(normalizedSearchTerm);
console.log("subscription_search: Created profile filter:", nFilter);
return nFilter;
default:
}
default: {
throw new Error(`Unknown search type: ${searchType}`);
}
}
}
@ -257,7 +258,7 @@ async function createProfileSearchFilter( @@ -257,7 +258,7 @@ async function createProfileSearchFilter(
subscriptionType: "npub-specific",
};
}
} catch (e) {
} catch {
// Not a valid npub, continue with other strategies
}
@ -277,11 +278,11 @@ async function createProfileSearchFilter( @@ -277,11 +278,11 @@ async function createProfileSearchFilter(
subscriptionType: "nip05-found",
};
}
} catch (e) {
} catch {
// Continue to next domain
}
}
} catch (e) {
} catch {
// Fallback to reasonable profile search
}
@ -520,12 +521,11 @@ function createSearchResult( @@ -520,12 +521,11 @@ function createSearchResult(
/**
* Search other relays in background
*/
async function searchOtherRelaysInBackground(
function searchOtherRelaysInBackground(
searchType: SearchSubscriptionType,
searchFilter: SearchFilter,
searchState: any,
callbacks?: SearchCallbacks,
abortSignal?: AbortSignal,
cleanup?: () => void,
): Promise<SearchResult> {
const ndk = get(ndkInstance);
@ -578,7 +578,7 @@ async function searchOtherRelaysInBackground( @@ -578,7 +578,7 @@ async function searchOtherRelaysInBackground(
} else {
processContentEvent(event, searchType, searchState);
}
} catch (e) {
} catch {
// Invalid JSON or other error, skip
}
});

15
src/routes/+layout.ts

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
import { getPersistedLogin, initNdk, ndkInstance } from "$lib/ndk";
import { getPersistedLogin, initNdk, ndkInstance } from "../lib/ndk.ts";
import {
loginWithExtension,
loginWithAmber,
loginWithNpub,
} from "$lib/stores/userStore";
import { loginMethodStorageKey } from "$lib/stores/userStore";
import Pharos, { pharosInstance } from "$lib/parser";
} from "../lib/stores/userStore.ts";
import { loginMethodStorageKey } from "../lib/stores/userStore.ts";
import Pharos, { pharosInstance } from "../lib/parser.ts";
import type { LayoutLoad } from "./$types";
import { get } from "svelte/store";
@ -35,10 +35,11 @@ export const load: LayoutLoad = () => { @@ -35,10 +35,11 @@ export const load: LayoutLoad = () => {
const localNsec = localStorage.getItem("amber/nsec");
if (localNsec) {
import("@nostr-dev-kit/ndk").then(
async ({ NDKNip46Signer, default: NDK }) => {
async ({ NDKNip46Signer }) => {
const ndk = get(ndkInstance);
try {
const amberSigner = NDKNip46Signer.nostrconnect(
// deno-lint-ignore no-explicit-any
const amberSigner = (NDKNip46Signer as any).nostrconnect(
ndk,
relay,
localNsec,
@ -52,7 +53,7 @@ export const load: LayoutLoad = () => { @@ -52,7 +53,7 @@ export const load: LayoutLoad = () => {
const user = await amberSigner.user();
await loginWithAmber(amberSigner, user);
console.log("Amber session restored.");
} catch (err) {
} catch {
// If reconnection fails, automatically fallback to npub-only mode
console.warn(
"Amber session could not be restored. Falling back to npub-only mode.",

6
src/routes/+page.svelte

@ -1,12 +1,8 @@ @@ -1,12 +1,8 @@
<script lang="ts">
import { Alert, Input } from "flowbite-svelte";
import { HammerSolid } from "flowbite-svelte-icons";
import { userStore } from "$lib/stores/userStore";
import { activeInboxRelays, ndkSignedIn } from "$lib/ndk";
import { Input } from "flowbite-svelte";
import PublicationFeed from "$lib/components/publications/PublicationFeed.svelte";
let searchQuery = $state("");
let user = $derived($userStore);
let eventCount = $state({ displayed: 0, total: 0 });
function handleEventCountUpdate(counts: { displayed: number; total: number }) {

15
src/routes/publication/+page.ts

@ -2,8 +2,9 @@ import { error } from "@sveltejs/kit"; @@ -2,8 +2,9 @@ import { error } from "@sveltejs/kit";
import type { Load } from "@sveltejs/kit";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
import { getActiveRelaySetAsNDKRelaySet } from "$lib/ndk";
import { getMatchingTags } from "$lib/utils/nostrUtils";
import { getActiveRelaySetAsNDKRelaySet } from "../../lib/ndk.ts";
import { getMatchingTags } from "../../lib/utils/nostrUtils.ts";
import type NDK from "@nostr-dev-kit/ndk";
/**
* Decodes an naddr identifier and returns a filter object
@ -30,7 +31,7 @@ function decodeNaddr(id: string) { @@ -30,7 +31,7 @@ function decodeNaddr(id: string) {
/**
* Fetches an event by ID or filter
*/
async function fetchEventById(ndk: any, id: string): Promise<NDKEvent> {
async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> {
const filter = decodeNaddr(id);
// Handle the case where filter is null (decoding error)
@ -66,7 +67,7 @@ async function fetchEventById(ndk: any, id: string): Promise<NDKEvent> { @@ -66,7 +67,7 @@ async function fetchEventById(ndk: any, id: string): Promise<NDKEvent> {
/**
* Fetches an event by d tag
*/
async function fetchEventByDTag(ndk: any, dTag: string): Promise<NDKEvent> {
async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> {
try {
const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays
const event = await ndk.fetchEvent(
@ -90,7 +91,7 @@ export const load: Load = async ({ @@ -90,7 +91,7 @@ export const load: Load = async ({
parent,
}: {
url: URL;
parent: () => Promise<any>;
parent: () => Promise<Partial<Record<string, NDK>>>;
}) => {
const id = url.searchParams.get("id");
const dTag = url.searchParams.get("d");
@ -102,8 +103,8 @@ export const load: Load = async ({ @@ -102,8 +103,8 @@ export const load: Load = async ({
// Fetch the event based on available parameters
const indexEvent = id
? await fetchEventById(ndk, id)
: await fetchEventByDTag(ndk, dTag!);
? await fetchEventById(ndk!, id)
: await fetchEventByDTag(ndk!, dTag!);
const publicationType = getMatchingTags(indexEvent, "type")[0]?.[1];

3
vite.config.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import { execSync } from "child_process";
import process from "node:process";
// Function to get the latest git tag
function getAppVersionString() {
@ -17,7 +18,7 @@ function getAppVersionString() { @@ -17,7 +18,7 @@ function getAppVersionString() {
// Get the latest git tag, assuming git is installed and tagged branch is available
const tag = execSync("git describe --tags --abbrev=0").toString().trim();
return tag;
} catch (error) {
} catch {
return "development";
}
}

Loading…
Cancel
Save