Browse Source

Ran `deno fmt`

master
buttercat1791 6 months ago
parent
commit
c06f102e4a
  1. 6
      postcss.config.js
  2. 24
      src/app.css
  3. 14
      src/app.html
  4. 13
      src/lib/a/README.md
  5. 65
      src/lib/components/event_input/eventServices.ts
  6. 2
      src/lib/components/event_input/types.ts
  7. 21
      src/lib/components/event_input/validation.ts
  8. 62
      src/lib/data_structures/publication_tree.ts
  9. 10
      src/lib/nostr/event.ts
  10. 23
      src/lib/nostr/format.ts
  11. 21
      src/lib/nostr/nip05.ts
  12. 148
      src/lib/nostr/nip58.ts
  13. 10
      src/lib/nostr/types.ts
  14. 14
      src/lib/stores/techStore.ts
  15. 12
      src/lib/stores/themeStore.ts
  16. 7
      src/lib/stores/userStore.ts
  17. 4
      src/lib/styles/cva.ts
  18. 3
      src/lib/utils/asciidoc_metadata.ts
  19. 26
      src/lib/utils/cache_manager.ts
  20. 7
      src/lib/utils/event_input_utils.ts
  21. 5
      src/lib/utils/event_search.ts
  22. 14
      src/lib/utils/markup/advancedMarkupParser.ts
  23. 12
      src/lib/utils/markup/basicMarkupParser.ts
  24. 5
      src/lib/utils/markup/embeddedMarkupParser.ts
  25. 57
      src/lib/utils/markup/markupUtils.ts
  26. 4
      src/lib/utils/nostrUtils.ts
  27. 67
      src/lib/utils/npubCache.ts
  28. 37
      src/lib/utils/profile_search.ts
  29. 226
      src/lib/utils/subscription_search.ts
  30. 73
      src/lib/utils/user_lists.ts
  31. 1
      src/routes/publication/[type]/[identifier]/+layout.server.ts
  32. 3
      src/routes/publication/[type]/[identifier]/+page.ts
  33. 8
      src/styles/notifications.css
  34. 3
      src/styles/scrollbar.css
  35. 71
      tests/unit/mathProcessing.test.ts
  36. 8
      tests/unit/tagExpansion.test.ts

6
postcss.config.js

@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {} "@tailwindcss/postcss": {},
} },
} };

24
src/app.css

@ -226,7 +226,8 @@
div.note-leather, div.note-leather,
p.note-leather, p.note-leather,
section.note-leather { section.note-leather {
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 p-2 rounded; @apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100
p-2 rounded;
} }
.edit div.note-leather:hover:not(:has(.note-leather:hover)), .edit div.note-leather:hover:not(:has(.note-leather:hover)),
@ -278,7 +279,8 @@
} }
div.modal-leather > div { div.modal-leather > div {
@apply bg-primary-50 dark:bg-primary-950 border-b-[1px] border-primary-100 dark:border-primary-600; @apply bg-primary-50 dark:bg-primary-950 border-b-[1px] border-primary-100
dark:border-primary-600;
} }
div.modal-leather > div > h1, div.modal-leather > div > h1,
@ -292,7 +294,9 @@
} }
div.modal-leather button { div.modal-leather button {
@apply bg-primary-50 hover:bg-primary-50 dark:bg-primary-950 dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600 dark:text-gray-100 dark:hover:text-primary-400; @apply bg-primary-50 hover:bg-primary-50 dark:bg-primary-950
dark:hover:bg-primary-950 text-gray-900 hover:text-primary-600
dark:text-gray-100 dark:hover:text-primary-400;
} }
/* Navbar */ /* Navbar */
@ -504,7 +508,9 @@
/* Tooltip */ /* Tooltip */
.tooltip-leather { .tooltip-leather {
@apply fixed p-4 rounded shadow-lg bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 transition-colors duration-200; @apply fixed p-4 rounded shadow-lg bg-primary-50 dark:bg-primary-1000
text-gray-900 dark:text-gray-100 border border-gray-200
dark:border-gray-700 transition-colors duration-200;
max-width: 400px; max-width: 400px;
z-index: 1000; z-index: 1000;
} }
@ -585,7 +591,8 @@
} }
a { a {
@apply underline cursor-pointer hover:text-primary-600 dark:hover:text-primary-400; @apply underline cursor-pointer hover:text-primary-600
dark:hover:text-primary-400;
} }
.imageblock { .imageblock {
@ -728,14 +735,15 @@
input[type="tel"], input[type="tel"],
input[type="url"], input[type="url"],
textarea { textarea {
@apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border-s-4 border-primary-200 rounded shadow-none; @apply bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100
border-s-4 border-primary-200 rounded shadow-none;
@apply focus:border-primary-600 dark:focus:border-primary-400; @apply focus:border-primary-600 dark:focus:border-primary-400;
} }
/* Table of Contents highlighting */ /* Table of Contents highlighting */
.toc-highlight { .toc-highlight {
@apply bg-primary-200 dark:bg-primary-700 border-s-4 border-primary-600 rounded @apply bg-primary-200 dark:bg-primary-700 border-s-4 border-primary-600
dark:border-primary-400 font-medium; rounded dark:border-primary-400 font-medium;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
} }

14
src/app.html

@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en" data-tech="off"> <html lang="en" data-tech="off">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@ -19,7 +19,9 @@
<script> <script>
try { try {
const v = localStorage.getItem("alexandria/showTech"); const v = localStorage.getItem("alexandria/showTech");
document.documentElement.dataset.tech = v === "true" ? "on" : "off"; document.documentElement.dataset.tech = v === "true"
? "on"
: "off";
} catch (_) { } catch (_) {
/* no-op */ /* no-op */
} }
@ -46,14 +48,18 @@
}, },
}; };
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> <script
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
></script>
<!-- highlight.js for code highlighting --> <!-- highlight.js for code highlighting -->
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
/> />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
></script>
%sveltekit.head% %sveltekit.head%
</head> </head>

13
src/lib/a/README.md

@ -1,10 +1,11 @@
# Component Library # Component Library
This folder contains a component library. This folder contains a component library. The idea is to have project-scoped
The idea is to have project-scoped reusable components that centralize theming and style rules, reusable components that centralize theming and style rules, so that main pages
so that main pages and layouts focus on the functionalities. and layouts focus on the functionalities.
All components are based on Flowbite Svelte components, All components are based on Flowbite Svelte components, which are built on top
which are built on top of Tailwind CSS. of Tailwind CSS.
Keeping all the styles in one place allows us to easily change the look and feel of the application by switching themes. Keeping all the styles in one place allows us to easily change the look and feel
of the application by switching themes.

65
src/lib/components/event_input/eventServices.ts

@ -14,21 +14,30 @@ import { anonymousRelays } from "$lib/consts";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk";
import { removeMetadataFromContent } from "$lib/utils/asciidoc_metadata"; import { removeMetadataFromContent } from "$lib/utils/asciidoc_metadata";
import { build30040EventSet } from "$lib/utils/event_input_utils"; import { build30040EventSet } from "$lib/utils/event_input_utils";
import type { EventData, TagData, PublishResult, LoadEventResult } from "./types"; import type {
EventData,
LoadEventResult,
PublishResult,
TagData,
} from "./types";
/** /**
* Converts TagData array to NDK-compatible format * Converts TagData array to NDK-compatible format
*/ */
function convertTagsToNDKFormat(tags: TagData[]): string[][] { function convertTagsToNDKFormat(tags: TagData[]): string[][] {
return tags return tags
.filter(tag => tag.key.trim() !== "") .filter((tag) => tag.key.trim() !== "")
.map(tag => [tag.key, ...tag.values]); .map((tag) => [tag.key, ...tag.values]);
} }
/** /**
* Publishes an event to relays * Publishes an event to relays
*/ */
export async function publishEvent(ndk: any, eventData: EventData, tags: TagData[]): Promise<PublishResult> { export async function publishEvent(
ndk: any,
eventData: EventData,
tags: TagData[],
): Promise<PublishResult> {
if (!ndk) { if (!ndk) {
return { success: false, error: "NDK context not available" }; return { success: false, error: "NDK context not available" };
} }
@ -42,7 +51,10 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
const pubkeyString = String(pubkey); const pubkeyString = String(pubkey);
if (!/^[a-fA-F0-9]{64}$/.test(pubkeyString)) { if (!/^[a-fA-F0-9]{64}$/.test(pubkeyString)) {
return { success: false, error: "Invalid public key: must be a 64-character hex string." }; return {
success: false,
error: "Invalid public key: must be a 64-character hex string.",
};
} }
const baseEvent = { pubkey: pubkeyString, created_at: eventData.createdAt }; const baseEvent = { pubkey: pubkeyString, created_at: eventData.createdAt };
@ -59,14 +71,18 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
try { try {
// Get the current d and title values from the UI // Get the current d and title values from the UI
const dTagValue = tags.find(tag => tag.key === "d")?.values[0] || ""; const dTagValue = tags.find((tag) => tag.key === "d")?.values[0] || "";
const titleTagValue = tags.find(tag => tag.key === "title")?.values[0] || ""; const titleTagValue = tags.find((tag) =>
tag.key === "title"
)?.values[0] || "";
// Convert multi-value tags to the format expected by build30040EventSet // Convert multi-value tags to the format expected by build30040EventSet
// Filter out d and title tags since we'll add them manually // Filter out d and title tags since we'll add them manually
const compatibleTags: [string, string][] = tags const compatibleTags: [string, string][] = tags
.filter(tag => tag.key.trim() !== "" && tag.key !== "d" && tag.key !== "title") .filter((tag) =>
.map(tag => [tag.key, tag.values[0] || ""] as [string, string]); tag.key.trim() !== "" && tag.key !== "d" && tag.key !== "title"
)
.map((tag) => [tag.key, tag.values[0] || ""] as [string, string]);
const { indexEvent, sectionEvents } = build30040EventSet( const { indexEvent, sectionEvents } = build30040EventSet(
eventData.content, eventData.content,
@ -76,7 +92,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
); );
// Override the d and title tags with the UI values if they exist // Override the d and title tags with the UI values if they exist
const finalTags = indexEvent.tags.filter(tag => tag[0] !== "d" && tag[0] !== "title"); const finalTags = indexEvent.tags.filter((tag) =>
tag[0] !== "d" && tag[0] !== "title"
);
if (dTagValue) { if (dTagValue) {
finalTags.push(["d", dTagValue]); finalTags.push(["d", dTagValue]);
} }
@ -97,7 +115,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
console.error("Error in build30040EventSet:", error); console.error("Error in build30040EventSet:", error);
return { return {
success: false, success: false,
error: `Failed to build 30040 event set: ${error instanceof Error ? error.message : "Unknown error"}` error: `Failed to build 30040 event set: ${
error instanceof Error ? error.message : "Unknown error"
}`,
}; };
} }
} else { } else {
@ -181,7 +201,10 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
console.log("publishEvent: Publishing to relays:", relays); console.log("publishEvent: Publishing to relays:", relays);
console.log("publishEvent: Anonymous relays:", anonymousRelays); console.log("publishEvent: Anonymous relays:", anonymousRelays);
console.log("publishEvent: Active outbox relays:", get(activeOutboxRelays)); console.log(
"publishEvent: Active outbox relays:",
get(activeOutboxRelays),
);
console.log("publishEvent: Active inbox relays:", get(activeInboxRelays)); console.log("publishEvent: Active inbox relays:", get(activeInboxRelays));
let published = false; let published = false;
@ -236,7 +259,9 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
console.error("Error signing/publishing event:", signError); console.error("Error signing/publishing event:", signError);
return { return {
success: false, success: false,
error: `Failed to sign event: ${signError instanceof Error ? signError.message : "Unknown error"}` error: `Failed to sign event: ${
signError instanceof Error ? signError.message : "Unknown error"
}`,
}; };
} }
} }
@ -245,7 +270,7 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
return { return {
success: true, success: true,
eventId: lastEventId || undefined, eventId: lastEventId || undefined,
relays: relaysPublished relays: relaysPublished,
}; };
} else { } else {
return { success: false, error: "Failed to publish to any relay." }; return { success: false, error: "Failed to publish to any relay." };
@ -255,13 +280,19 @@ export async function publishEvent(ndk: any, eventData: EventData, tags: TagData
/** /**
* Loads an event by its hex ID * Loads an event by its hex ID
*/ */
export async function loadEvent(ndk: any, eventId: string): Promise<LoadEventResult | null> { export async function loadEvent(
ndk: any,
eventId: string,
): Promise<LoadEventResult | null> {
if (!ndk) { if (!ndk) {
throw new Error("NDK context not available"); throw new Error("NDK context not available");
} }
console.log("loadEvent: Starting search for event ID:", eventId); console.log("loadEvent: Starting search for event ID:", eventId);
console.log("loadEvent: NDK pool relays:", Array.from(ndk.pool.relays.values()).map((r: any) => r.url)); console.log(
"loadEvent: NDK pool relays:",
Array.from(ndk.pool.relays.values()).map((r: any) => r.url),
);
console.log("loadEvent: Active inbox relays:", get(activeInboxRelays)); console.log("loadEvent: Active inbox relays:", get(activeInboxRelays));
console.log("loadEvent: Active outbox relays:", get(activeOutboxRelays)); console.log("loadEvent: Active outbox relays:", get(activeOutboxRelays));
@ -279,7 +310,7 @@ export async function loadEvent(ndk: any, eventId: string): Promise<LoadEventRes
// Convert NDK tags format to our format // Convert NDK tags format to our format
const tags: TagData[] = foundEvent.tags.map((tag: string[]) => ({ const tags: TagData[] = foundEvent.tags.map((tag: string[]) => ({
key: tag[0] || "", key: tag[0] || "",
values: tag.slice(1) values: tag.slice(1),
})); }));
return { eventData, tags }; return { eventData, tags };

2
src/lib/components/event_input/types.ts

@ -32,7 +32,7 @@ export interface LoadEventResult {
} }
export interface EventPreview { export interface EventPreview {
type: 'standard_event' | '30040_index_event' | 'error'; type: "standard_event" | "30040_index_event" | "error";
event?: { event?: {
id: string; id: string;
pubkey: string; pubkey: string;

21
src/lib/components/event_input/validation.ts

@ -6,15 +6,18 @@ import { get } from "svelte/store";
import { userStore } from "$lib/stores/userStore"; import { userStore } from "$lib/stores/userStore";
import type { EventData, TagData, ValidationResult } from "./types"; import type { EventData, TagData, ValidationResult } from "./types";
import { import {
validateNotAsciidoc,
validateAsciiDoc,
validate30040EventSet, validate30040EventSet,
validateAsciiDoc,
validateNotAsciidoc,
} from "$lib/utils/event_input_utils"; } from "$lib/utils/event_input_utils";
/** /**
* Validates an event and its tags * Validates an event and its tags
*/ */
export function validateEvent(eventData: EventData, tags: TagData[]): ValidationResult { export function validateEvent(
eventData: EventData,
tags: TagData[],
): ValidationResult {
const userState = get(userStore); const userState = get(userStore);
const pubkey = userState.pubkey; const pubkey = userState.pubkey;
@ -35,11 +38,13 @@ export function validateEvent(eventData: EventData, tags: TagData[]): Validation
if (eventData.kind === 30040) { if (eventData.kind === 30040) {
// Check for required tags // Check for required tags
const versionTag = tags.find(t => t.key === "version"); const versionTag = tags.find((t) => t.key === "version");
const dTag = tags.find(t => t.key === "d"); const dTag = tags.find((t) => t.key === "d");
const titleTag = tags.find(t => t.key === "title"); const titleTag = tags.find((t) => t.key === "title");
if (!versionTag || !versionTag.values[0] || versionTag.values[0].trim() === "") { if (
!versionTag || !versionTag.values[0] || versionTag.values[0].trim() === ""
) {
return { valid: false, reason: "30040 events require a 'version' tag." }; return { valid: false, reason: "30040 events require a 'version' tag." };
} }
@ -86,5 +91,5 @@ export function isValidTagKey(key: string): boolean {
* Validates that a tag has at least one value * Validates that a tag has at least one value
*/ */
export function isValidTag(tag: TagData): boolean { export function isValidTag(tag: TagData): boolean {
return isValidTagKey(tag.key) && tag.values.some(v => v.trim().length > 0); return isValidTagKey(tag.key) && tag.values.some((v) => v.trim().length > 0);
} }

62
src/lib/data_structures/publication_tree.ts

@ -253,7 +253,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// Clear all nodes except the root to force fresh loading // Clear all nodes except the root to force fresh loading
const rootAddress = this.#root.address; const rootAddress = this.#root.address;
this.#nodes.clear(); this.#nodes.clear();
this.#nodes.set(rootAddress, new Lazy<PublicationTreeNode>(() => Promise.resolve(this.#root))); this.#nodes.set(
rootAddress,
new Lazy<PublicationTreeNode>(() => Promise.resolve(this.#root)),
);
// Clear events cache to ensure fresh data // Clear events cache to ensure fresh data
this.#events.clear(); this.#events.clear();
this.#eventCache.clear(); this.#eventCache.clear();
@ -501,7 +504,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// AI-NOTE: Check if this node has already been visited // AI-NOTE: Check if this node has already been visited
if (this.#visitedNodes.has(address)) { if (this.#visitedNodes.has(address)) {
console.debug(`[PublicationTree] Skipping already visited node: ${address}`); console.debug(
`[PublicationTree] Skipping already visited node: ${address}`,
);
return { done: false, value: null }; return { done: false, value: null };
} }
@ -762,7 +767,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
#addNode(address: string, parentNode: PublicationTreeNode) { #addNode(address: string, parentNode: PublicationTreeNode) {
// AI-NOTE: Add debugging to track node addition // AI-NOTE: Add debugging to track node addition
console.debug(`[PublicationTree] Adding node ${address} to parent ${parentNode.address}`); console.debug(
`[PublicationTree] Adding node ${address} to parent ${parentNode.address}`,
);
const lazyNode = new Lazy<PublicationTreeNode>(() => const lazyNode = new Lazy<PublicationTreeNode>(() =>
this.#resolveNode(address, parentNode) this.#resolveNode(address, parentNode)
@ -902,7 +909,11 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
this.#eventCache.set(address, fetchedEvent); this.#eventCache.set(address, fetchedEvent);
this.#events.set(address, fetchedEvent); this.#events.set(address, fetchedEvent);
return await this.#buildNodeFromEvent(fetchedEvent, address, parentNode); return await this.#buildNodeFromEvent(
fetchedEvent,
address,
parentNode,
);
} }
} catch (error) { } catch (error) {
console.debug( console.debug(
@ -1017,7 +1028,9 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// AI-NOTE: Remove e-tag processing from synchronous method // AI-NOTE: Remove e-tag processing from synchronous method
// E-tags should be resolved asynchronously in #resolveNode method // E-tags should be resolved asynchronously in #resolveNode method
// Adding raw event IDs here causes duplicate processing // Adding raw event IDs here causes duplicate processing
console.debug(`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`); console.debug(
`[PublicationTree] Found ${eTags.length} e-tags but skipping processing in buildNodeFromEvent`,
);
} }
const node: PublicationTreeNode = { const node: PublicationTreeNode = {
@ -1033,13 +1046,18 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// Now directly adds child nodes to current node's children array // Now directly adds child nodes to current node's children array
// Add children in the order they appear in the a-tags to preserve section order // Add children in the order they appear in the a-tags to preserve section order
// Use sequential processing to ensure order is maintained // Use sequential processing to ensure order is maintained
console.log(`[PublicationTree] Adding ${childAddresses.length} children in order:`, childAddresses); console.log(
`[PublicationTree] Adding ${childAddresses.length} children in order:`,
childAddresses,
);
for (const childAddress of childAddresses) { for (const childAddress of childAddresses) {
console.log(`[PublicationTree] Adding child: ${childAddress}`); console.log(`[PublicationTree] Adding child: ${childAddress}`);
try { try {
// Add the child node directly to the current node's children // Add the child node directly to the current node's children
this.#addNode(childAddress, node); this.#addNode(childAddress, node);
console.log(`[PublicationTree] Successfully added child: ${childAddress}`); console.log(
`[PublicationTree] Successfully added child: ${childAddress}`,
);
} catch (error) { } catch (error) {
console.warn( console.warn(
`[PublicationTree] Error adding child ${childAddress} for ${node.address}:`, `[PublicationTree] Error adding child ${childAddress} for ${node.address}:`,
@ -1061,23 +1079,43 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
// Check if this 30040 has any children (a-tags only, since e-tags are handled separately) // Check if this 30040 has any children (a-tags only, since e-tags are handled separately)
const hasChildren = event.tags.some((tag) => tag[0] === "a"); const hasChildren = event.tags.some((tag) => tag[0] === "a");
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`); console.debug(
`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${
event.tags.find((t) => t[0] === "d")?.[1]
} - hasChildren: ${hasChildren}, type: ${
hasChildren ? "Branch" : "Leaf"
}`,
);
return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf; return hasChildren
? PublicationTreeNodeType.Branch
: PublicationTreeNodeType.Leaf;
} }
// Zettel kinds are always leaves // Zettel kinds are always leaves
if ([30041, 30818, 30023].includes(event.kind)) { if ([30041, 30818, 30023].includes(event.kind)) {
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - Zettel kind, type: Leaf`); console.debug(
`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${
event.tags.find((t) => t[0] === "d")?.[1]
} - Zettel kind, type: Leaf`,
);
return PublicationTreeNodeType.Leaf; return PublicationTreeNodeType.Leaf;
} }
// For other kinds, check if they have children (a-tags only) // For other kinds, check if they have children (a-tags only)
const hasChildren = event.tags.some((tag) => tag[0] === "a"); const hasChildren = event.tags.some((tag) => tag[0] === "a");
console.debug(`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${event.tags.find(t => t[0] === 'd')?.[1]} - hasChildren: ${hasChildren}, type: ${hasChildren ? 'Branch' : 'Leaf'}`); console.debug(
`[PublicationTree] Node type for ${event.kind}:${event.pubkey}:${
event.tags.find((t) => t[0] === "d")?.[1]
} - hasChildren: ${hasChildren}, type: ${
hasChildren ? "Branch" : "Leaf"
}`,
);
return hasChildren ? PublicationTreeNodeType.Branch : PublicationTreeNodeType.Leaf; return hasChildren
? PublicationTreeNodeType.Branch
: PublicationTreeNodeType.Leaf;
} }
// #endregion // #endregion

10
src/lib/nostr/event.ts

@ -1 +1,9 @@
export type NostrEvent = { id:string; kind:number; pubkey:string; created_at:number; tags:string[][]; content:string; }; export type AddressPointer = string; export type NostrEvent = {
id: string;
kind: number;
pubkey: string;
created_at: number;
tags: string[][];
content: string;
};
export type AddressPointer = string;

23
src/lib/nostr/format.ts

@ -1 +1,22 @@
export function shortenBech32(id:string, keepPrefix=true, head=8, tail=6){ if(!id) return ''; const i=id.indexOf('1'); const prefix=i>0? id.slice(0,i):''; const data=i>0? id.slice(i+1): id; const short = data.length>head+tail ? `${'${'}data.slice(0,head)}…${'${'}data.slice(-tail)}` : data; return keepPrefix && prefix ? `${'${'}prefix}1${'${'}short}` : short; } export function displayNameFrom(npub:string, p?:{ name?:string; display_name?:string; nip05?:string }){ return (p?.display_name?.trim() || p?.name?.trim() || (p?.nip05 && p.nip05.split('@')[0]) || shortenBech32(npub,true)); } export function shortenBech32(
id: string,
keepPrefix = true,
head = 8,
tail = 6,
) {
if (!id) return "";
const i = id.indexOf("1");
const prefix = i > 0 ? id.slice(0, i) : "";
const data = i > 0 ? id.slice(i + 1) : id;
const short = data.length > head + tail
? `${"${"}data.slice(0,head)}…${"${"}data.slice(-tail)}`
: data;
return keepPrefix && prefix ? `${"${"}prefix}1${"${"}short}` : short;
}
export function displayNameFrom(
npub: string,
p?: { name?: string; display_name?: string; nip05?: string },
) {
return (p?.display_name?.trim() || p?.name?.trim() ||
(p?.nip05 && p.nip05.split("@")[0]) || shortenBech32(npub, true));
}

21
src/lib/nostr/nip05.ts

@ -1 +1,20 @@
export async function verifyNip05(nip05:string, pubkeyHex:string):Promise<boolean>{ try{ if(!nip05||!pubkeyHex) return false; const [name,domain]=nip05.toLowerCase().split('@'); if(!name||!domain) return false; const url=`https://${'${'}domain}/.well-known/nostr.json?name=${'${'}encodeURIComponent(name)}`; const res=await fetch(url,{ headers:{ Accept:'application/json' } }); if(!res.ok) return false; const json=await res.json(); const found=json?.names?.[name]; return typeof found==='string' && found.toLowerCase()===pubkeyHex.toLowerCase(); }catch{ return false; } } export async function verifyNip05(
nip05: string,
pubkeyHex: string,
): Promise<boolean> {
try {
if (!nip05 || !pubkeyHex) return false;
const [name, domain] = nip05.toLowerCase().split("@");
if (!name || !domain) return false;
const url =
`https://${"${"}domain}/.well-known/nostr.json?name=${"${"}encodeURIComponent(name)}`;
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (!res.ok) return false;
const json = await res.json();
const found = json?.names?.[name];
return typeof found === "string" &&
found.toLowerCase() === pubkeyHex.toLowerCase();
} catch {
return false;
}
}

148
src/lib/nostr/nip58.ts

@ -1 +1,147 @@
import type { NostrEvent, AddressPointer } from './event'; export type BadgeDefinition={ kind:30009; id:string; pubkey:string; d:string; a:AddressPointer; name?:string; description?:string; image?:{ url:string; size?:string }|null; thumbs:{ url:string; size?:string }[]; }; export type BadgeAward={ kind:8; id:string; pubkey:string; a:AddressPointer; recipients:{ pubkey:string; relay?:string }[]; }; export type ProfileBadges={ kind:30008; id:string; pubkey:string; pairs:{ a:AddressPointer; awardId:string; relay?:string }[]; }; export const isKind=(e:NostrEvent,k:number)=>e.kind===k; const val=(tags:string[][],name:string)=>tags.find(t=>t[0]===name)?.[1]; const vals=(tags:string[][],name:string)=>tags.filter(t=>t[0]===name).map(t=>t.slice(1)); export function parseBadgeDefinition(e:NostrEvent):BadgeDefinition|null{ if(e.kind!==30009) return null; const d=val(e.tags,'d'); if(!d) return null; const a:AddressPointer=`30009:${'${'}e.pubkey}:${'${'}d}`; const name=val(e.tags,'name')||undefined; const description=val(e.tags,'description')||undefined; const imageTag=vals(e.tags,'image')[0]; const image=imageTag? { url:imageTag[0], size:imageTag[1] }: null; const thumbs=vals(e.tags,'thumb').map(([url,size])=>({ url, size })); return { kind:30009, id:e.id, pubkey:e.pubkey, d, a, name, description, image, thumbs }; } export function parseBadgeAward(e:NostrEvent):BadgeAward|null{ if(e.kind!==8) return null; const atag=vals(e.tags,'a')[0]; if(!atag) return null; const a:AddressPointer=atag[0]; const recipients=vals(e.tags,'p').map(([pubkey,relay])=>({ pubkey, relay })); return { kind:8, id:e.id, pubkey:e.pubkey, a, recipients }; } export function parseProfileBadges(e:NostrEvent):ProfileBadges|null{ if(e.kind!==30008) return null; const d=val(e.tags,'d'); if(d!=='profile_badges') return null; const pairs: { a:AddressPointer; awardId:string; relay?:string }[]=[]; for(let i=0;i<e.tags.length;i++){ const t=e.tags[i]; if(t[0]==='a'){ const a=t[1]; const nxt=e.tags[i+1]; if(nxt && nxt[0]==='e'){ pairs.push({ a, awardId:nxt[1], relay:nxt[2] }); i++; } } } return { kind:30008, id:e.id, pubkey:e.pubkey, pairs }; } export type DisplayBadge={ def:BadgeDefinition; award:BadgeAward|null; issuer:string; thumbUrl:string|null; title:string; }; export function pickThumb(def:BadgeDefinition, prefer:( '16'|'32'|'64'|'256'|'512')[]=['32','64','256']):string|null{ for(const p of prefer){ const t=def.thumbs.find(t=>(t.size||'').startsWith(p+'x')); if(t) return t.url; } return def.image?.url || null; } export function buildDisplayBadgesForUser(userPubkey:string, defs:BadgeDefinition[], awards:BadgeAward[], profileBadges?:ProfileBadges|null, opts:{ issuerWhitelist?:Set<string>; max?:number }={}):DisplayBadge[]{ const byA=new Map<string,BadgeDefinition>(defs.map(d=>[d.a,d])); const byAwardId=new Map<string,BadgeAward>(awards.map(a=>[a.id,a])); const isWhitelisted=(issuer:string)=>!opts.issuerWhitelist || opts.issuerWhitelist.has(issuer); let out:DisplayBadge[]=[]; if(profileBadges && profileBadges.pubkey===userPubkey){ for(const {a,awardId} of profileBadges.pairs){ const def=byA.get(a); if(!def) continue; const award=byAwardId.get(awardId)||null; if(award && (award.a!==a || !award.recipients.find(r=>r.pubkey===userPubkey))) continue; if(!isWhitelisted(def.pubkey)) continue; out.push({ def, award, issuer:def.pubkey, thumbUrl: pickThumb(def), title: def.name || def.d }); } } else { for(const aw of awards){ if(!aw.recipients.find(r=>r.pubkey===userPubkey)) continue; const def=byA.get(aw.a); if(!def) continue; if(!isWhitelisted(def.pubkey)) continue; out.push({ def, award:aw, issuer:def.pubkey, thumbUrl: pickThumb(def), title: def.name || def.d }); } } if(opts.max && out.length>opts.max) out=out.slice(0,opts.max); return out; } import type { AddressPointer, NostrEvent } from "./event";
export type BadgeDefinition = {
kind: 30009;
id: string;
pubkey: string;
d: string;
a: AddressPointer;
name?: string;
description?: string;
image?: { url: string; size?: string } | null;
thumbs: { url: string; size?: string }[];
};
export type BadgeAward = {
kind: 8;
id: string;
pubkey: string;
a: AddressPointer;
recipients: { pubkey: string; relay?: string }[];
};
export type ProfileBadges = {
kind: 30008;
id: string;
pubkey: string;
pairs: { a: AddressPointer; awardId: string; relay?: string }[];
};
export const isKind = (e: NostrEvent, k: number) => e.kind === k;
const val = (tags: string[][], name: string) =>
tags.find((t) => t[0] === name)?.[1];
const vals = (tags: string[][], name: string) =>
tags.filter((t) => t[0] === name).map((t) => t.slice(1));
export function parseBadgeDefinition(e: NostrEvent): BadgeDefinition | null {
if (e.kind !== 30009) return null;
const d = val(e.tags, "d");
if (!d) return null;
const a: AddressPointer = `30009:${"${"}e.pubkey}:${"${"}d}`;
const name = val(e.tags, "name") || undefined;
const description = val(e.tags, "description") || undefined;
const imageTag = vals(e.tags, "image")[0];
const image = imageTag ? { url: imageTag[0], size: imageTag[1] } : null;
const thumbs = vals(e.tags, "thumb").map(([url, size]) => ({ url, size }));
return {
kind: 30009,
id: e.id,
pubkey: e.pubkey,
d,
a,
name,
description,
image,
thumbs,
};
}
export function parseBadgeAward(e: NostrEvent): BadgeAward | null {
if (e.kind !== 8) return null;
const atag = vals(e.tags, "a")[0];
if (!atag) return null;
const a: AddressPointer = atag[0];
const recipients = vals(e.tags, "p").map(([pubkey, relay]) => ({
pubkey,
relay,
}));
return { kind: 8, id: e.id, pubkey: e.pubkey, a, recipients };
}
export function parseProfileBadges(e: NostrEvent): ProfileBadges | null {
if (e.kind !== 30008) return null;
const d = val(e.tags, "d");
if (d !== "profile_badges") return null;
const pairs: { a: AddressPointer; awardId: string; relay?: string }[] = [];
for (let i = 0; i < e.tags.length; i++) {
const t = e.tags[i];
if (t[0] === "a") {
const a = t[1];
const nxt = e.tags[i + 1];
if (nxt && nxt[0] === "e") {
pairs.push({ a, awardId: nxt[1], relay: nxt[2] });
i++;
}
}
}
return { kind: 30008, id: e.id, pubkey: e.pubkey, pairs };
}
export type DisplayBadge = {
def: BadgeDefinition;
award: BadgeAward | null;
issuer: string;
thumbUrl: string | null;
title: string;
};
export function pickThumb(
def: BadgeDefinition,
prefer: ("16" | "32" | "64" | "256" | "512")[] = ["32", "64", "256"],
): string | null {
for (const p of prefer) {
const t = def.thumbs.find((t) => (t.size || "").startsWith(p + "x"));
if (t) return t.url;
}
return def.image?.url || null;
}
export function buildDisplayBadgesForUser(
userPubkey: string,
defs: BadgeDefinition[],
awards: BadgeAward[],
profileBadges?: ProfileBadges | null,
opts: { issuerWhitelist?: Set<string>; max?: number } = {},
): DisplayBadge[] {
const byA = new Map<string, BadgeDefinition>(defs.map((d) => [d.a, d]));
const byAwardId = new Map<string, BadgeAward>(awards.map((a) => [a.id, a]));
const isWhitelisted = (issuer: string) =>
!opts.issuerWhitelist || opts.issuerWhitelist.has(issuer);
let out: DisplayBadge[] = [];
if (profileBadges && profileBadges.pubkey === userPubkey) {
for (const { a, awardId } of profileBadges.pairs) {
const def = byA.get(a);
if (!def) {
continue;
}
const award = byAwardId.get(awardId) || null;
if (
award &&
(award.a !== a ||
!award.recipients.find((r) => r.pubkey === userPubkey))
) continue;
if (!isWhitelisted(def.pubkey)) continue;
out.push({
def,
award,
issuer: def.pubkey,
thumbUrl: pickThumb(def),
title: def.name || def.d,
});
}
} else {for (const aw of awards) {
if (!aw.recipients.find((r) => r.pubkey === userPubkey)) continue;
const def = byA.get(aw.a);
if (!def) continue;
if (!isWhitelisted(def.pubkey)) continue;
out.push({
def,
award: aw,
issuer: def.pubkey,
thumbUrl: pickThumb(def),
title: def.name || def.d,
});
}}
if (opts.max && out.length > opts.max) out = out.slice(0, opts.max);
return out;
}

10
src/lib/nostr/types.ts

@ -1 +1,9 @@
export type NostrProfile = { name?:string; display_name?:string; picture?:string; about?:string; nip05?:string; lud16?:string; badges?: Array<{ label:string; color?:string }>; }; export type NostrProfile = {
name?: string;
display_name?: string;
picture?: string;
about?: string;
nip05?: string;
lud16?: string;
badges?: Array<{ label: string; color?: string }>;
};

14
src/lib/stores/techStore.ts

@ -1,16 +1,16 @@
import { writable } from 'svelte/store'; import { writable } from "svelte/store";
const KEY = 'alexandria/showTech'; const KEY = "alexandria/showTech";
// Default false unless explicitly set to 'true' in localStorage // Default false unless explicitly set to 'true' in localStorage
const initial = typeof localStorage !== 'undefined' const initial = typeof localStorage !== "undefined"
? localStorage.getItem(KEY) === 'true' ? localStorage.getItem(KEY) === "true"
: false; : false;
export const showTech = writable<boolean>(initial); export const showTech = writable<boolean>(initial);
showTech.subscribe(v => { showTech.subscribe((v) => {
if (typeof document !== 'undefined') { if (typeof document !== "undefined") {
document.documentElement.dataset.tech = v ? 'on' : 'off'; document.documentElement.dataset.tech = v ? "on" : "off";
localStorage.setItem(KEY, String(v)); localStorage.setItem(KEY, String(v));
} }
}); });

12
src/lib/stores/themeStore.ts

@ -1,15 +1,15 @@
import { writable } from 'svelte/store'; import { writable } from "svelte/store";
const KEY = 'alexandria/theme'; const KEY = "alexandria/theme";
const initial = const initial =
(typeof localStorage !== 'undefined' && localStorage.getItem(KEY)) || (typeof localStorage !== "undefined" && localStorage.getItem(KEY)) ||
'light'; "light";
export const theme = writable(initial); export const theme = writable(initial);
theme.subscribe(v => { theme.subscribe((v) => {
if (typeof document !== 'undefined') { if (typeof document !== "undefined") {
document.documentElement.dataset.theme = String(v); document.documentElement.dataset.theme = String(v);
localStorage.setItem(KEY, String(v)); localStorage.setItem(KEY, String(v));
} }

7
src/lib/stores/userStore.ts

@ -16,7 +16,6 @@ import {
import { loginStorageKey } from "../consts.ts"; import { loginStorageKey } from "../consts.ts";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
export interface UserState { export interface UserState {
pubkey: string | null; pubkey: string | null;
npub: string | null; npub: string | null;
@ -248,7 +247,11 @@ export async function loginWithExtension(ndk: NDK) {
/** /**
* Login with Amber (NIP-46) * Login with Amber (NIP-46)
*/ */
export async function loginWithAmber(amberSigner: NDKSigner, user: NDKUser, ndk: NDK) { export async function loginWithAmber(
amberSigner: NDKSigner,
user: NDKUser,
ndk: NDK,
) {
if (!ndk) throw new Error("NDK not initialized"); if (!ndk) throw new Error("NDK not initialized");
// Only clear previous login state after successful login // Only clear previous login state after successful login
const npub = user.npub; const npub = user.npub;

4
src/lib/styles/cva.ts

@ -1,3 +1,3 @@
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export { cva, twMerge, type VariantProps }; export { cva, twMerge, type VariantProps };

3
src/lib/utils/asciidoc_metadata.ts

@ -293,7 +293,8 @@ function stripSectionHeader(sectionContent: string): string {
!line.includes("<") && !line.includes("<") &&
!line.match(/^:[^:]+:\s*.+$/) && !line.match(/^:[^:]+:\s*.+$/) &&
line.trim() !== "" && line.trim() !== "" &&
!(line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && line.trim().split(/\s+/).length <= 2) !(line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" &&
line.trim().split(/\s+/).length <= 2)
) { ) {
contentStart = i; contentStart = i;
break; break;

26
src/lib/utils/cache_manager.ts

@ -1,7 +1,7 @@
import { unifiedProfileCache } from './npubCache'; import { unifiedProfileCache } from "./npubCache";
import { searchCache } from './searchCache'; import { searchCache } from "./searchCache";
import { indexEventCache } from './indexEventCache'; import { indexEventCache } from "./indexEventCache";
import { clearRelaySetCache } from '../ndk'; import { clearRelaySetCache } from "../ndk";
/** /**
* Clears all application caches * Clears all application caches
@ -13,7 +13,7 @@ import { clearRelaySetCache } from '../ndk';
* - relaySetCache (relay configuration) * - relaySetCache (relay configuration)
*/ */
export function clearAllCaches(): void { export function clearAllCaches(): void {
console.log('[CacheManager] Clearing all application caches...'); console.log("[CacheManager] Clearing all application caches...");
// Clear in-memory caches // Clear in-memory caches
unifiedProfileCache.clear(); unifiedProfileCache.clear();
@ -24,7 +24,7 @@ export function clearAllCaches(): void {
// Clear localStorage caches // Clear localStorage caches
clearLocalStorageCaches(); clearLocalStorageCaches();
console.log('[CacheManager] All caches cleared successfully'); console.log("[CacheManager] All caches cleared successfully");
} }
/** /**
@ -32,7 +32,7 @@ export function clearAllCaches(): void {
* This is useful when profile pictures or metadata are stale * This is useful when profile pictures or metadata are stale
*/ */
export function clearProfileCaches(): void { export function clearProfileCaches(): void {
console.log('[CacheManager] Clearing profile-specific caches...'); console.log("[CacheManager] Clearing profile-specific caches...");
// Clear unified profile cache // Clear unified profile cache
unifiedProfileCache.clear(); unifiedProfileCache.clear();
@ -42,31 +42,33 @@ export function clearProfileCaches(): void {
// This is acceptable since profile searches are the most common // This is acceptable since profile searches are the most common
searchCache.clear(); searchCache.clear();
console.log('[CacheManager] Profile caches cleared successfully'); console.log("[CacheManager] Profile caches cleared successfully");
} }
/** /**
* Clears localStorage caches * Clears localStorage caches
*/ */
function clearLocalStorageCaches(): void { function clearLocalStorageCaches(): void {
if (typeof window === 'undefined') return; if (typeof window === "undefined") return;
const keysToRemove: string[] = []; const keysToRemove: string[] = [];
// Find all localStorage keys that start with 'alexandria' // Find all localStorage keys that start with 'alexandria'
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i);
if (key && key.startsWith('alexandria')) { if (key && key.startsWith("alexandria")) {
keysToRemove.push(key); keysToRemove.push(key);
} }
} }
// Remove the keys // Remove the keys
keysToRemove.forEach(key => { keysToRemove.forEach((key) => {
localStorage.removeItem(key); localStorage.removeItem(key);
}); });
console.log(`[CacheManager] Cleared ${keysToRemove.length} localStorage items`); console.log(
`[CacheManager] Cleared ${keysToRemove.length} localStorage items`,
);
} }
/** /**

7
src/lib/utils/event_input_utils.ts

@ -204,7 +204,6 @@ function extractMarkdownTopHeader(content: string): string | null {
// Event Construction // Event Construction
// ========================= // =========================
/** /**
* Builds a set of events for a 30040 publication: one 30040 index event and one 30041 event per section. * Builds a set of events for a 30040 publication: one 30040 index event and one 30041 event per section.
* Each 30041 gets a d-tag (normalized section header) and a title tag (raw section header). * Each 30041 gets a d-tag (normalized section header) and a title tag (raw section header).
@ -261,7 +260,8 @@ export function build30040EventSet(
console.log("Index event:", { documentTitle, indexDTag }); console.log("Index event:", { documentTitle, indexDTag });
// Create section events with their metadata // Create section events with their metadata
const sectionEvents: NDKEvent[] = parsed.sections.map((section: any, i: number) => { const sectionEvents: NDKEvent[] = parsed.sections.map(
(section: any, i: number) => {
const sectionDTag = `${indexDTag}-${normalizeDTagValue(section.title)}`; const sectionDTag = `${indexDTag}-${normalizeDTagValue(section.title)}`;
console.log(`Creating section ${i}:`, { console.log(`Creating section ${i}:`, {
title: section.title, title: section.title,
@ -285,7 +285,8 @@ export function build30040EventSet(
pubkey: baseEvent.pubkey, pubkey: baseEvent.pubkey,
created_at: baseEvent.created_at, created_at: baseEvent.created_at,
}); });
}); },
);
// Create proper a tags with format: kind:pubkey:d-tag // Create proper a tags with format: kind:pubkey:d-tag
const aTags = sectionEvents.map((event) => { const aTags = sectionEvents.map((event) => {

5
src/lib/utils/event_search.ts

@ -10,7 +10,10 @@ import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
/** /**
* Search for a single event by ID or filter * Search for a single event by ID or filter
*/ */
export async function searchEvent(query: string, ndk: NDK): Promise<NDKEvent | null> { export async function searchEvent(
query: string,
ndk: NDK,
): Promise<NDKEvent | null> {
if (!ndk) { if (!ndk) {
console.warn("[Search] No NDK instance available"); console.warn("[Search] No NDK instance available");
return null; return null;

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

@ -406,23 +406,29 @@ function processInlineCodeMath(content: string): string {
} }
// Process display math ($$...$$) first to avoid conflicts with inline math // Process display math ($$...$$) first to avoid conflicts with inline math
let processedContent = codeContent.replace(/\$\$([\s\S]*?)\$\$/g, (mathMatch: string, mathContent: string) => { let processedContent = codeContent.replace(
/\$\$([\s\S]*?)\$\$/g,
(mathMatch: string, mathContent: string) => {
// Skip empty math expressions // Skip empty math expressions
if (!mathContent.trim()) { if (!mathContent.trim()) {
return mathMatch; return mathMatch;
} }
return `<span class="math-display">\\[${mathContent}\\]</span>`; return `<span class="math-display">\\[${mathContent}\\]</span>`;
}); },
);
// Process inline math ($...$) after display math // Process inline math ($...$) after display math
// Use a more sophisticated regex that handles escaped dollar signs // Use a more sophisticated regex that handles escaped dollar signs
processedContent = processedContent.replace(/\$((?:[^$\\]|\\.)*?)\$/g, (mathMatch: string, mathContent: string) => { processedContent = processedContent.replace(
/\$((?:[^$\\]|\\.)*?)\$/g,
(mathMatch: string, mathContent: string) => {
// Skip empty math expressions // Skip empty math expressions
if (!mathContent.trim()) { if (!mathContent.trim()) {
return mathMatch; return mathMatch;
} }
return `<span class="math-inline">\\(${mathContent}\\)</span>`; return `<span class="math-inline">\\(${mathContent}\\)</span>`;
}); },
);
return `\`${processedContent}\``; return `\`${processedContent}\``;
}); });

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

@ -7,8 +7,6 @@ import {
processWikilinks, processWikilinks,
} from "./markupUtils.ts"; } from "./markupUtils.ts";
export function preProcessBasicMarkup(text: string): string { export function preProcessBasicMarkup(text: string): string {
try { try {
// Process basic text formatting first // Process basic text formatting first
@ -26,7 +24,10 @@ export function preProcessBasicMarkup(text: string): string {
} }
} }
export async function postProcessBasicMarkup(text: string, ndk?: NDK): Promise<string> { export async function postProcessBasicMarkup(
text: string,
ndk?: NDK,
): Promise<string> {
try { try {
// Process Nostr identifiers last // Process Nostr identifiers last
let processedText = await processNostrIdentifiersInText(text, ndk); let processedText = await processNostrIdentifiersInText(text, ndk);
@ -40,7 +41,10 @@ export async function postProcessBasicMarkup(text: string, ndk?: NDK): Promise<s
} }
} }
export async function parseBasicMarkup(text: string, ndk?: NDK): Promise<string> { export async function parseBasicMarkup(
text: string,
ndk?: NDK,
): Promise<string> {
if (!text) return ""; if (!text) return "";
try { try {

5
src/lib/utils/markup/embeddedMarkupParser.ts

@ -1,4 +1,7 @@
import { postProcessBasicMarkup, preProcessBasicMarkup } from "./basicMarkupParser.ts"; import {
postProcessBasicMarkup,
preProcessBasicMarkup,
} from "./basicMarkupParser.ts";
import { processNostrIdentifiersWithEmbeddedEvents } from "./markupUtils.ts"; import { processNostrIdentifiersWithEmbeddedEvents } from "./markupUtils.ts";
/** /**

57
src/lib/utils/markup/markupUtils.ts

@ -80,7 +80,10 @@ export function replaceAlexandriaNostrLinks(text: string): string {
return text; return text;
} }
export function renderListGroup(lines: string[], typeHint?: "ol" | "ul"): string { export function renderListGroup(
lines: string[],
typeHint?: "ol" | "ul",
): string {
function parseList( function parseList(
start: number, start: number,
indent: number, indent: number,
@ -322,7 +325,9 @@ export async function processNostrIdentifiersInText(
metadata = await getUserMetadata(identifier, ndk); metadata = await getUserMetadata(identifier, ndk);
} else { } else {
// Fallback when NDK is not available - just use the identifier // Fallback when NDK is not available - just use the identifier
metadata = { name: identifier.slice(0, 8) + "..." + identifier.slice(-4) }; metadata = {
name: identifier.slice(0, 8) + "..." + identifier.slice(-4),
};
} }
const displayText = metadata.displayName || metadata.name; const displayText = metadata.displayName || metadata.name;
const link = createProfileLink(identifier, displayText); const link = createProfileLink(identifier, displayText);
@ -391,14 +396,18 @@ export function processAllNostrIdentifiers(text: string): string {
// Pattern for prefixed nostr identifiers (nostr:npub1, nostr:note1, etc.) // Pattern for prefixed nostr identifiers (nostr:npub1, nostr:note1, etc.)
// This handles both full identifiers and partial ones that might appear in content // This handles both full identifiers and partial ones that might appear in content
const prefixedNostrPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g; const prefixedNostrPattern =
/nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{20,}/g;
// Pattern for bare nostr identifiers (npub1, note1, nevent1, naddr1) // Pattern for bare nostr identifiers (npub1, note1, nevent1, naddr1)
// Exclude matches that are part of URLs to avoid breaking existing links // Exclude matches that are part of URLs to avoid breaking existing links
const bareNostrPattern = /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{20,}/g; const bareNostrPattern =
/(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{20,}/g;
// Process prefixed nostr identifiers first // Process prefixed nostr identifiers first
const prefixedMatches = Array.from(processedText.matchAll(prefixedNostrPattern)); const prefixedMatches = Array.from(
processedText.matchAll(prefixedNostrPattern),
);
// Process them in reverse order to avoid index shifting issues // Process them in reverse order to avoid index shifting issues
for (let i = prefixedMatches.length - 1; i >= 0; i--) { for (let i = prefixedMatches.length - 1; i >= 0; i--) {
@ -407,11 +416,12 @@ export function processAllNostrIdentifiers(text: string): string {
const matchIndex = match.index ?? 0; const matchIndex = match.index ?? 0;
// Create shortened display text // Create shortened display text
const identifier = fullMatch.replace('nostr:', ''); const identifier = fullMatch.replace("nostr:", "");
const displayText = `${identifier.slice(0, 8)}...${identifier.slice(-4)}`; const displayText = `${identifier.slice(0, 8)}...${identifier.slice(-4)}`;
// Create clickable link // Create clickable link
const replacement = `<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`; const replacement =
`<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
// Replace the match in the text // Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement + processedText = processedText.slice(0, matchIndex) + replacement +
@ -431,7 +441,8 @@ export function processAllNostrIdentifiers(text: string): string {
const displayText = `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}`; const displayText = `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}`;
// Create clickable link with nostr: prefix for the href // Create clickable link with nostr: prefix for the href
const replacement = `<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`; const replacement =
`<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
// Replace the match in the text // Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement + processedText = processedText.slice(0, matchIndex) + replacement +
@ -439,8 +450,11 @@ export function processAllNostrIdentifiers(text: string): string {
} }
// Also handle any remaining truncated prefixed identifiers that might be cut off or incomplete // Also handle any remaining truncated prefixed identifiers that might be cut off or incomplete
const truncatedPrefixedPattern = /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{8,}/g; const truncatedPrefixedPattern =
const truncatedPrefixedMatches = Array.from(processedText.matchAll(truncatedPrefixedPattern)); /nostr:(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]{8,}/g;
const truncatedPrefixedMatches = Array.from(
processedText.matchAll(truncatedPrefixedPattern),
);
for (let i = truncatedPrefixedMatches.length - 1; i >= 0; i--) { for (let i = truncatedPrefixedMatches.length - 1; i >= 0; i--) {
const match = truncatedPrefixedMatches[i]; const match = truncatedPrefixedMatches[i];
@ -451,11 +465,14 @@ export function processAllNostrIdentifiers(text: string): string {
if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars
// Create display text for truncated identifiers // Create display text for truncated identifiers
const identifier = fullMatch.replace('nostr:', ''); const identifier = fullMatch.replace("nostr:", "");
const displayText = identifier.length > 12 ? `${identifier.slice(0, 8)}...${identifier.slice(-4)}` : identifier; const displayText = identifier.length > 12
? `${identifier.slice(0, 8)}...${identifier.slice(-4)}`
: identifier;
// Create clickable link // Create clickable link
const replacement = `<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`; const replacement =
`<a href="/events?id=${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="${fullMatch}">${displayText}</a>`;
// Replace the match in the text // Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement + processedText = processedText.slice(0, matchIndex) + replacement +
@ -463,8 +480,11 @@ export function processAllNostrIdentifiers(text: string): string {
} }
// Handle truncated bare identifiers // Handle truncated bare identifiers
const truncatedBarePattern = /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{8,}/g; const truncatedBarePattern =
const truncatedBareMatches = Array.from(processedText.matchAll(truncatedBarePattern)); /(?<!https?:\/\/[^\s]*)(?<!wss?:\/\/[^\s]*)(?<!nostr:)(npub1|note1|nevent1|naddr1)[a-zA-Z0-9]{8,}/g;
const truncatedBareMatches = Array.from(
processedText.matchAll(truncatedBarePattern),
);
for (let i = truncatedBareMatches.length - 1; i >= 0; i--) { for (let i = truncatedBareMatches.length - 1; i >= 0; i--) {
const match = truncatedBareMatches[i]; const match = truncatedBareMatches[i];
@ -475,10 +495,13 @@ export function processAllNostrIdentifiers(text: string): string {
if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars if (fullMatch.length >= 30) continue; // Full identifiers are at least 30 chars
// Create display text for truncated identifiers // Create display text for truncated identifiers
const displayText = fullMatch.length > 12 ? `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}` : fullMatch; const displayText = fullMatch.length > 12
? `${fullMatch.slice(0, 8)}...${fullMatch.slice(-4)}`
: fullMatch;
// Create clickable link // Create clickable link
const replacement = `<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`; const replacement =
`<a href="/events?id=nostr:${fullMatch}" class="text-primary-600 dark:text-primary-500 hover:underline break-all" title="nostr:${fullMatch}">${displayText}</a>`;
// Replace the match in the text // Replace the match in the text
processedText = processedText.slice(0, matchIndex) + replacement + processedText = processedText.slice(0, matchIndex) + replacement +

4
src/lib/utils/nostrUtils.ts

@ -7,9 +7,9 @@ import type { Filter } from "./search_types.ts";
import { import {
anonymousRelays, anonymousRelays,
communityRelays, communityRelays,
localRelays,
searchRelays, searchRelays,
secondaryRelays, secondaryRelays,
localRelays,
} from "../consts.ts"; } from "../consts.ts";
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk"; import { NDKRelaySet as NDKRelaySetFromNDK } from "@nostr-dev-kit/ndk";
@ -220,7 +220,7 @@ export async function processNostrIdentifiers(
]; ];
const combinedContext = beforeContext + afterContext; const combinedContext = beforeContext + afterContext;
return urlPatterns.some(pattern => pattern.test(combinedContext)); return urlPatterns.some((pattern) => pattern.test(combinedContext));
} }
// Process profiles (npub and nprofile) // Process profiles (npub and nprofile)

67
src/lib/utils/npubCache.ts

@ -59,7 +59,11 @@ class UnifiedProfileCache {
/** /**
* Get profile data, fetching fresh data if needed * Get profile data, fetching fresh data if needed
*/ */
async getProfile(identifier: string, ndk?: NDK, force = false): Promise<NpubMetadata> { async getProfile(
identifier: string,
ndk?: NDK,
force = false,
): Promise<NpubMetadata> {
const cleanId = identifier.replace(/^nostr:/, ""); const cleanId = identifier.replace(/^nostr:/, "");
// Check cache first (unless forced) // Check cache first (unless forced)
@ -81,8 +85,13 @@ class UnifiedProfileCache {
/** /**
* Fetch profile from all available relays and cache it * Fetch profile from all available relays and cache it
*/ */
private async fetchAndCacheProfile(identifier: string, ndk?: NDK): Promise<NpubMetadata> { private async fetchAndCacheProfile(
const fallback = { name: `${identifier.slice(0, 8)}...${identifier.slice(-4)}` }; identifier: string,
ndk?: NDK,
): Promise<NpubMetadata> {
const fallback = {
name: `${identifier.slice(0, 8)}...${identifier.slice(-4)}`,
};
try { try {
if (!ndk) { if (!ndk) {
@ -92,7 +101,10 @@ class UnifiedProfileCache {
const decoded = nip19.decode(identifier); const decoded = nip19.decode(identifier);
if (!decoded) { if (!decoded) {
console.warn("UnifiedProfileCache: Failed to decode identifier:", identifier); console.warn(
"UnifiedProfileCache: Failed to decode identifier:",
identifier,
);
return fallback; return fallback;
} }
@ -103,11 +115,17 @@ class UnifiedProfileCache {
} else if (decoded.type === "nprofile") { } else if (decoded.type === "nprofile") {
pubkey = decoded.data.pubkey; pubkey = decoded.data.pubkey;
} else { } else {
console.warn("UnifiedProfileCache: Unsupported identifier type:", decoded.type); console.warn(
"UnifiedProfileCache: Unsupported identifier type:",
decoded.type,
);
return fallback; return fallback;
} }
console.log("UnifiedProfileCache: Fetching fresh profile for pubkey:", pubkey); console.log(
"UnifiedProfileCache: Fetching fresh profile for pubkey:",
pubkey,
);
// Use fetchEventWithFallback to search ALL available relays // Use fetchEventWithFallback to search ALL available relays
const profileEvent = await fetchEventWithFallback(ndk, { const profileEvent = await fetchEventWithFallback(ndk, {
@ -116,7 +134,10 @@ class UnifiedProfileCache {
}); });
if (!profileEvent || !profileEvent.content) { if (!profileEvent || !profileEvent.content) {
console.warn("UnifiedProfileCache: No profile event found for:", pubkey); console.warn(
"UnifiedProfileCache: No profile event found for:",
pubkey,
);
return fallback; return fallback;
} }
@ -147,7 +168,6 @@ class UnifiedProfileCache {
console.log("UnifiedProfileCache: Cached fresh profile:", metadata); console.log("UnifiedProfileCache: Cached fresh profile:", metadata);
return metadata; return metadata;
} catch (e) { } catch (e) {
console.error("UnifiedProfileCache: Error fetching profile:", e); console.error("UnifiedProfileCache: Error fetching profile:", e);
return fallback; return fallback;
@ -177,7 +197,12 @@ class UnifiedProfileCache {
/** /**
* Set profile data in cache * Set profile data in cache
*/ */
set(identifier: string, profile: NpubMetadata, pubkey?: string, relaySource?: string): void { set(
identifier: string,
profile: NpubMetadata,
pubkey?: string,
relaySource?: string,
): void {
const cleanId = identifier.replace(/^nostr:/, ""); const cleanId = identifier.replace(/^nostr:/, "");
const entry: CacheEntry = { const entry: CacheEntry = {
profile, profile,
@ -271,11 +296,13 @@ class UnifiedProfileCache {
} }
} }
expiredKeys.forEach(key => this.cache.delete(key)); expiredKeys.forEach((key) => this.cache.delete(key));
if (expiredKeys.length > 0) { if (expiredKeys.length > 0) {
this.saveToStorage(); this.saveToStorage();
console.log(`UnifiedProfileCache: Cleaned up ${expiredKeys.length} expired entries`); console.log(
`UnifiedProfileCache: Cleaned up ${expiredKeys.length} expired entries`,
);
} }
} }
} }
@ -294,7 +321,8 @@ if (typeof window !== "undefined") {
// but make it use the unified cache internally // but make it use the unified cache internally
export const npubCache = { export const npubCache = {
get: (key: string) => unifiedProfileCache.getCached(key), get: (key: string) => unifiedProfileCache.getCached(key),
set: (key: string, value: NpubMetadata) => unifiedProfileCache.set(key, value), set: (key: string, value: NpubMetadata) =>
unifiedProfileCache.set(key, value),
has: (key: string) => unifiedProfileCache.has(key), has: (key: string) => unifiedProfileCache.has(key),
delete: (key: string) => unifiedProfileCache.delete(key), delete: (key: string) => unifiedProfileCache.delete(key),
clear: () => unifiedProfileCache.clear(), clear: () => unifiedProfileCache.clear(),
@ -303,14 +331,19 @@ export const npubCache = {
}; };
// Legacy compatibility for old profileCache functions // Legacy compatibility for old profileCache functions
export async function getDisplayName(pubkey: string, ndk: NDK): Promise<string> { export async function getDisplayName(
pubkey: string,
ndk: NDK,
): Promise<string> {
const profile = await unifiedProfileCache.getProfile(pubkey, ndk); const profile = await unifiedProfileCache.getProfile(pubkey, ndk);
return profile.displayName || profile.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; return profile.displayName || profile.name ||
`${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
} }
export function getDisplayNameSync(pubkey: string): string { export function getDisplayNameSync(pubkey: string): string {
const profile = unifiedProfileCache.getCached(pubkey); const profile = unifiedProfileCache.getCached(pubkey);
return profile?.displayName || profile?.name || `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`; return profile?.displayName || profile?.name ||
`${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`;
} }
export async function batchFetchProfiles( export async function batchFetchProfiles(
@ -340,8 +373,8 @@ export async function batchFetchProfiles(
}); });
const results = await Promise.allSettled(fetchPromises); const results = await Promise.allSettled(fetchPromises);
results.forEach(result => { results.forEach((result) => {
if (result.status === 'fulfilled' && result.value) { if (result.status === "fulfilled" && result.value) {
allProfileEvents.push(result.value); allProfileEvents.push(result.value);
} }
}); });

37
src/lib/utils/profile_search.ts

@ -1,8 +1,17 @@
import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts"; import { activeInboxRelays, activeOutboxRelays } from "../ndk.ts";
import { getNpubFromNip05, getUserMetadata, fetchEventWithFallback } from "./nostrUtils.ts"; import {
fetchEventWithFallback,
getNpubFromNip05,
getUserMetadata,
} from "./nostrUtils.ts";
import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk";
import { searchCache } from "./searchCache.ts"; import { searchCache } from "./searchCache.ts";
import { communityRelays, searchRelays, secondaryRelays, anonymousRelays } from "../consts.ts"; import {
anonymousRelays,
communityRelays,
searchRelays,
secondaryRelays,
} from "../consts.ts";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { NostrProfile, ProfileSearchResult } from "./search_types.ts"; import type { NostrProfile, ProfileSearchResult } from "./search_types.ts";
import { import {
@ -94,7 +103,10 @@ export async function searchProfiles(
} }
} }
} catch (e) { } catch (e) {
console.warn("profile_search: Failed to fetch original event timestamp:", e); console.warn(
"profile_search: Failed to fetch original event timestamp:",
e,
);
} }
const profile: NostrProfile & { created_at?: number } = { const profile: NostrProfile & { created_at?: number } = {
@ -223,7 +235,10 @@ async function searchNip05Domains(
} }
} }
} catch (e) { } catch (e) {
console.warn("profile_search: Failed to fetch original event timestamp:", e); console.warn(
"profile_search: Failed to fetch original event timestamp:",
e,
);
} }
const profile: NostrProfile & { created_at?: number } = { const profile: NostrProfile & { created_at?: number } = {
@ -275,7 +290,10 @@ async function searchNip05Domains(
} }
} }
} catch (e) { } catch (e) {
console.warn("profile_search: Failed to fetch original event timestamp:", e); console.warn(
"profile_search: Failed to fetch original event timestamp:",
e,
);
} }
const profile: NostrProfile & { created_at?: number } = { const profile: NostrProfile & { created_at?: number } = {
@ -330,7 +348,9 @@ async function quickRelaySearch(
// This ensures we don't miss profiles due to stale cache or limited relay coverage // This ensures we don't miss profiles due to stale cache or limited relay coverage
// Get all available relays from NDK pool (most comprehensive) // Get all available relays from NDK pool (most comprehensive)
const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) => r.url) as string[]; const poolRelays = Array.from(ndk.pool.relays.values()).map((r: any) =>
r.url
) as string[];
const userInboxRelays = get(activeInboxRelays); const userInboxRelays = get(activeInboxRelays);
const userOutboxRelays = get(activeOutboxRelays); const userOutboxRelays = get(activeOutboxRelays);
@ -347,7 +367,10 @@ async function quickRelaySearch(
// Deduplicate relay URLs // Deduplicate relay URLs
const uniqueRelayUrls = [...new Set(allRelayUrls)]; const uniqueRelayUrls = [...new Set(allRelayUrls)];
console.log("Using ALL available relays for profile search:", uniqueRelayUrls); console.log(
"Using ALL available relays for profile search:",
uniqueRelayUrls,
);
console.log("Total relays for profile search:", uniqueRelayUrls.length); console.log("Total relays for profile search:", uniqueRelayUrls.length);
// Create relay sets for parallel search // Create relay sets for parallel search

226
src/lib/utils/subscription_search.ts

@ -58,7 +58,7 @@ async function prioritizeSearchEvents(
events: NDKEvent[], events: NDKEvent[],
targetPubkey?: string, targetPubkey?: string,
maxResults: number = SEARCH_LIMITS.GENERAL_CONTENT, maxResults: number = SEARCH_LIMITS.GENERAL_CONTENT,
ndk?: NDK ndk?: NDK,
): Promise<NDKEvent[]> { ): Promise<NDKEvent[]> {
if (events.length === 0) { if (events.length === 0) {
return []; return [];
@ -72,7 +72,9 @@ async function prioritizeSearchEvents(
if (ndk) { if (ndk) {
try { try {
// Import user list functions dynamically to avoid circular dependencies // Import user list functions dynamically to avoid circular dependencies
const { fetchCurrentUserLists, getPubkeysFromListKind } = await import("./user_lists.ts"); const { fetchCurrentUserLists, getPubkeysFromListKind } = await import(
"./user_lists.ts"
);
const { checkCommunity } = await import("./community_checker.ts"); const { checkCommunity } = await import("./community_checker.ts");
// Get current user's follow lists (if logged in) // Get current user's follow lists (if logged in)
@ -80,10 +82,14 @@ async function prioritizeSearchEvents(
userFollowPubkeys = getPubkeysFromListKind(userLists, 3); // Kind 3 = follow list userFollowPubkeys = getPubkeysFromListKind(userLists, 3); // Kind 3 = follow list
// Check community status for unique pubkeys in events (limit to prevent hanging) // Check community status for unique pubkeys in events (limit to prevent hanging)
const uniquePubkeys = new Set(events.map(e => e.pubkey).filter(Boolean)); const uniquePubkeys = new Set(
events.map((e) => e.pubkey).filter(Boolean),
);
const pubkeysToCheck = Array.from(uniquePubkeys).slice(0, 20); // Limit to first 20 pubkeys const pubkeysToCheck = Array.from(uniquePubkeys).slice(0, 20); // Limit to first 20 pubkeys
console.log(`subscription_search: Checking community status for ${pubkeysToCheck.length} pubkeys out of ${uniquePubkeys.size} total`); console.log(
`subscription_search: Checking community status for ${pubkeysToCheck.length} pubkeys out of ${uniquePubkeys.size} total`,
);
const communityChecks = await Promise.allSettled( const communityChecks = await Promise.allSettled(
pubkeysToCheck.map(async (pubkey) => { pubkeysToCheck.map(async (pubkey) => {
@ -91,19 +97,25 @@ async function prioritizeSearchEvents(
const isCommunityMember = await Promise.race([ const isCommunityMember = await Promise.race([
checkCommunity(pubkey), checkCommunity(pubkey),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Community check timeout')), 2000) setTimeout(
() => reject(new Error("Community check timeout")),
2000,
) )
),
]); ]);
return { pubkey, isCommunityMember }; return { pubkey, isCommunityMember };
} catch (error) { } catch (error) {
console.warn(`subscription_search: Community check failed for ${pubkey}:`, error); console.warn(
`subscription_search: Community check failed for ${pubkey}:`,
error,
);
return { pubkey, isCommunityMember: false }; return { pubkey, isCommunityMember: false };
} }
}) }),
); );
// Build set of community member pubkeys // Build set of community member pubkeys
communityChecks.forEach(result => { communityChecks.forEach((result) => {
if (result.status === "fulfilled" && result.value.isCommunityMember) { if (result.status === "fulfilled" && result.value.isCommunityMember) {
communityMemberPubkeys.add(result.value.pubkey); communityMemberPubkeys.add(result.value.pubkey);
} }
@ -112,13 +124,18 @@ async function prioritizeSearchEvents(
console.log("subscription_search: Prioritization data loaded:", { console.log("subscription_search: Prioritization data loaded:", {
userFollows: userFollowPubkeys.size, userFollows: userFollowPubkeys.size,
communityMembers: communityMemberPubkeys.size, communityMembers: communityMemberPubkeys.size,
totalEvents: events.length totalEvents: events.length,
}); });
} catch (error) { } catch (error) {
console.warn("subscription_search: Failed to load prioritization data:", error); console.warn(
"subscription_search: Failed to load prioritization data:",
error,
);
} }
} else { } else {
console.log("subscription_search: No NDK provided, skipping user list and community checks"); console.log(
"subscription_search: No NDK provided, skipping user list and community checks",
);
} }
// Separate events into priority tiers // Separate events into priority tiers
@ -131,7 +148,9 @@ async function prioritizeSearchEvents(
const isFromTarget = targetPubkey && event.pubkey === targetPubkey; const isFromTarget = targetPubkey && event.pubkey === targetPubkey;
const isPrioritizedKind = PRIORITIZED_EVENT_KINDS.has(event.kind || 0); const isPrioritizedKind = PRIORITIZED_EVENT_KINDS.has(event.kind || 0);
const isFromFollow = userFollowPubkeys.has(event.pubkey || ""); const isFromFollow = userFollowPubkeys.has(event.pubkey || "");
const isFromCommunityMember = communityMemberPubkeys.has(event.pubkey || ""); const isFromCommunityMember = communityMemberPubkeys.has(
event.pubkey || "",
);
// AI-NOTE: Prioritized kinds are always in tier 1 // AI-NOTE: Prioritized kinds are always in tier 1
// Target pubkey priority only applies to n: searches (when targetPubkey is provided) // Target pubkey priority only applies to n: searches (when targetPubkey is provided)
@ -181,7 +200,7 @@ async function prioritizeSearchEvents(
tier2: tier2.length, // User follows tier2: tier2.length, // User follows
tier3: tier3.length, // Community members tier3: tier3.length, // Community members
tier4: tier4.length, // Others tier4: tier4.length, // Others
total: result.length total: result.length,
}); });
return result; return result;
@ -224,25 +243,34 @@ export async function searchBySubscription(
// AI-NOTE: Ensure cached events have created_at property preserved // AI-NOTE: Ensure cached events have created_at property preserved
// This fixes the "Unknown date" issue when events are retrieved from cache // This fixes the "Unknown date" issue when events are retrieved from cache
const eventsWithCreatedAt = cachedResult.events.map(event => { const eventsWithCreatedAt = cachedResult.events.map((event) => {
if (event && typeof event === 'object' && !event.created_at) { if (event && typeof event === "object" && !event.created_at) {
console.warn("subscription_search: Event missing created_at, setting to 0:", event.id); console.warn(
"subscription_search: Event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0; (event as any).created_at = 0;
} }
return event; return event;
}); });
const secondOrderWithCreatedAt = cachedResult.secondOrder.map(event => { const secondOrderWithCreatedAt = cachedResult.secondOrder.map((event) => {
if (event && typeof event === 'object' && !event.created_at) { if (event && typeof event === "object" && !event.created_at) {
console.warn("subscription_search: Second order event missing created_at, setting to 0:", event.id); console.warn(
"subscription_search: Second order event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0; (event as any).created_at = 0;
} }
return event; return event;
}); });
const tTagEventsWithCreatedAt = cachedResult.tTagEvents.map(event => { const tTagEventsWithCreatedAt = cachedResult.tTagEvents.map((event) => {
if (event && typeof event === 'object' && !event.created_at) { if (event && typeof event === "object" && !event.created_at) {
console.warn("subscription_search: T-tag event missing created_at, setting to 0:", event.id); console.warn(
"subscription_search: T-tag event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0; (event as any).created_at = 0;
} }
return event; return event;
@ -252,18 +280,22 @@ export async function searchBySubscription(
...cachedResult, ...cachedResult,
events: eventsWithCreatedAt, events: eventsWithCreatedAt,
secondOrder: secondOrderWithCreatedAt, secondOrder: secondOrderWithCreatedAt,
tTagEvents: tTagEventsWithCreatedAt tTagEvents: tTagEventsWithCreatedAt,
}; };
// AI-NOTE: Return cached results immediately but trigger second-order search in background // AI-NOTE: Return cached results immediately but trigger second-order search in background
// This ensures we get fast results while still updating second-order data // This ensures we get fast results while still updating second-order data
console.log("subscription_search: Returning cached result immediately, triggering background second-order search"); console.log(
"subscription_search: Returning cached result immediately, triggering background second-order search",
);
// Trigger second-order search in background for all search types // Trigger second-order search in background for all search types
if (ndk) { if (ndk) {
// Start second-order search in background for n and d searches only // Start second-order search in background for n and d searches only
if (searchType === "n" || searchType === "d") { if (searchType === "n" || searchType === "d") {
console.log("subscription_search: Triggering background second-order search for cached result"); console.log(
"subscription_search: Triggering background second-order search for cached result",
);
performSecondOrderSearchInBackground( performSecondOrderSearchInBackground(
searchType as "n" | "d", searchType as "n" | "d",
eventsWithCreatedAt, eventsWithCreatedAt,
@ -271,7 +303,7 @@ export async function searchBySubscription(
cachedResult.addresses || new Set(), cachedResult.addresses || new Set(),
ndk, ndk,
searchType === "n" ? eventsWithCreatedAt[0]?.pubkey : undefined, searchType === "n" ? eventsWithCreatedAt[0]?.pubkey : undefined,
callbacks callbacks,
); );
} }
} }
@ -316,7 +348,10 @@ export async function searchBySubscription(
// AI-NOTE: Check for preloaded events first (for profile searches) // AI-NOTE: Check for preloaded events first (for profile searches)
if (searchFilter.preloadedEvents && searchFilter.preloadedEvents.length > 0) { if (searchFilter.preloadedEvents && searchFilter.preloadedEvents.length > 0) {
console.log("subscription_search: Using preloaded events:", searchFilter.preloadedEvents.length); console.log(
"subscription_search: Using preloaded events:",
searchFilter.preloadedEvents.length,
);
processPrimaryRelayResults( processPrimaryRelayResults(
new Set(searchFilter.preloadedEvents), new Set(searchFilter.preloadedEvents),
searchType, searchType,
@ -328,7 +363,9 @@ export async function searchBySubscription(
); );
if (hasResults(searchState, searchType)) { if (hasResults(searchState, searchType)) {
console.log("subscription_search: Found results from preloaded events, returning immediately"); console.log(
"subscription_search: Found results from preloaded events, returning immediately",
);
const immediateResult = createSearchResult( const immediateResult = createSearchResult(
searchState, searchState,
searchType, searchType,
@ -376,10 +413,16 @@ export async function searchBySubscription(
); );
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Primary relay search timeout")), TIMEOUTS.SUBSCRIPTION_SEARCH); setTimeout(
() => reject(new Error("Primary relay search timeout")),
TIMEOUTS.SUBSCRIPTION_SEARCH,
);
}); });
const primaryEvents = await Promise.race([primaryEventsPromise, timeoutPromise]) as any; const primaryEvents = await Promise.race([
primaryEventsPromise,
timeoutPromise,
]) as any;
console.log( console.log(
"subscription_search: Primary relay returned", "subscription_search: Primary relay returned",
@ -473,10 +516,16 @@ export async function searchBySubscription(
); );
const fallbackTimeoutPromise = new Promise((_, reject) => { const fallbackTimeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Fallback search timeout")), TIMEOUTS.SUBSCRIPTION_SEARCH); setTimeout(
() => reject(new Error("Fallback search timeout")),
TIMEOUTS.SUBSCRIPTION_SEARCH,
);
}); });
const fallbackEvents = await Promise.race([fallbackEventsPromise, fallbackTimeoutPromise]) as any; const fallbackEvents = await Promise.race([
fallbackEventsPromise,
fallbackTimeoutPromise,
]) as any;
console.log( console.log(
"subscription_search: Fallback search returned", "subscription_search: Fallback search returned",
@ -520,8 +569,13 @@ export async function searchBySubscription(
); );
// If it's a timeout error, continue to return empty result // If it's a timeout error, continue to return empty result
if (fallbackError instanceof Error && fallbackError.message.includes("timeout")) { if (
console.log("subscription_search: Fallback search timed out, returning empty result"); fallbackError instanceof Error &&
fallbackError.message.includes("timeout")
) {
console.log(
"subscription_search: Fallback search timed out, returning empty result",
);
} }
} }
@ -556,7 +610,9 @@ export async function searchBySubscription(
// If it's a timeout error, continue to Phase 2 instead of failing // If it's a timeout error, continue to Phase 2 instead of failing
if (error instanceof Error && error.message.includes("timeout")) { if (error instanceof Error && error.message.includes("timeout")) {
console.log("subscription_search: Primary relay search timed out, continuing to Phase 2"); console.log(
"subscription_search: Primary relay search timed out, continuing to Phase 2",
);
} else { } else {
// For other errors, we might want to fail the search // For other errors, we might want to fail the search
throw error; throw error;
@ -685,7 +741,11 @@ async function createSearchFilter(
hexPubkey = decoded.data as string; hexPubkey = decoded.data as string;
} }
} catch (e) { } catch (e) {
console.warn("subscription_search: Failed to decode npub:", profile.pubkey, e); console.warn(
"subscription_search: Failed to decode npub:",
profile.pubkey,
e,
);
} }
} }
event.pubkey = hexPubkey; event.pubkey = hexPubkey;
@ -695,11 +755,17 @@ async function createSearchFilter(
// This ensures the profile cards show the actual creation date instead of "Unknown date" // This ensures the profile cards show the actual creation date instead of "Unknown date"
if ((profile as any).created_at) { if ((profile as any).created_at) {
event.created_at = (profile as any).created_at; event.created_at = (profile as any).created_at;
console.log("subscription_search: Using preserved timestamp:", event.created_at); console.log(
"subscription_search: Using preserved timestamp:",
event.created_at,
);
} else { } else {
// Fallback to current timestamp if no preserved timestamp // Fallback to current timestamp if no preserved timestamp
event.created_at = Math.floor(Date.now() / 1000); event.created_at = Math.floor(Date.now() / 1000);
console.log("subscription_search: Using fallback timestamp:", event.created_at); console.log(
"subscription_search: Using fallback timestamp:",
event.created_at,
);
} }
return event; return event;
@ -712,7 +778,10 @@ async function createSearchFilter(
searchTerm: normalizedSearchTerm, searchTerm: normalizedSearchTerm,
preloadedEvents: events, // AI-NOTE: Pass preloaded events preloadedEvents: events, // AI-NOTE: Pass preloaded events
}; };
console.log("subscription_search: Created profile filter with preloaded events:", nFilter); console.log(
"subscription_search: Created profile filter with preloaded events:",
nFilter,
);
return nFilter; return nFilter;
} }
default: { default: {
@ -721,8 +790,6 @@ async function createSearchFilter(
} }
} }
/** /**
* Create primary relay set for search operations * Create primary relay set for search operations
* AI-NOTE: Updated to use all available relays to prevent search failures * AI-NOTE: Updated to use all available relays to prevent search failures
@ -816,7 +883,9 @@ function processPrimaryRelayResults(
for (const event of events) { for (const event of events) {
// Check if we've reached the event limit // Check if we've reached the event limit
if (processedCount >= maxEvents) { if (processedCount >= maxEvents) {
console.log(`subscription_search: Reached event limit of ${maxEvents} in primary relay processing`); console.log(
`subscription_search: Reached event limit of ${maxEvents} in primary relay processing`,
);
break; break;
} }
@ -1029,7 +1098,9 @@ function searchOtherRelaysInBackground(
sub.on("event", (event: NDKEvent) => { sub.on("event", (event: NDKEvent) => {
// Check if we've reached the event limit // Check if we've reached the event limit
if (eventCount >= maxEvents) { if (eventCount >= maxEvents) {
console.log(`subscription_search: Reached event limit of ${maxEvents}, stopping event processing`); console.log(
`subscription_search: Reached event limit of ${maxEvents}, stopping event processing`,
);
sub.stop(); sub.stop();
return; return;
} }
@ -1058,7 +1129,9 @@ function searchOtherRelaysInBackground(
// Add timeout to prevent hanging // Add timeout to prevent hanging
const timeoutId = setTimeout(async () => { const timeoutId = setTimeout(async () => {
if (!resolved) { if (!resolved) {
console.log("subscription_search: Background search timeout, resolving with current results"); console.log(
"subscription_search: Background search timeout, resolving with current results",
);
resolved = true; resolved = true;
sub.stop(); sub.stop();
const result = await processEoseResults( const result = await processEoseResults(
@ -1106,7 +1179,12 @@ async function processEoseResults(
if (searchType === "n") { if (searchType === "n") {
return processProfileEoseResults(searchState, searchFilter, ndk, callbacks); return processProfileEoseResults(searchState, searchFilter, ndk, callbacks);
} else if (searchType === "d") { } else if (searchType === "d") {
return await processContentEoseResults(searchState, searchType, ndk, callbacks); return await processContentEoseResults(
searchState,
searchType,
ndk,
callbacks,
);
} else if (searchType === "t") { } else if (searchType === "t") {
return await processTTagEoseResults(searchState, ndk); return await processTTagEoseResults(searchState, ndk);
} }
@ -1242,7 +1320,7 @@ async function processContentEoseResults(
dedupedEvents, dedupedEvents,
undefined, // No specific target pubkey for d-tag searches undefined, // No specific target pubkey for d-tag searches
SEARCH_LIMITS.GENERAL_CONTENT, SEARCH_LIMITS.GENERAL_CONTENT,
ndk ndk,
); );
// AI-NOTE: Attach profile data to first-order events for display // AI-NOTE: Attach profile data to first-order events for display
@ -1276,7 +1354,10 @@ async function processContentEoseResults(
/** /**
* Process t-tag EOSE results * Process t-tag EOSE results
*/ */
async function processTTagEoseResults(searchState: any, ndk?: NDK): Promise<SearchResult> { async function processTTagEoseResults(
searchState: any,
ndk?: NDK,
): Promise<SearchResult> {
if (searchState.tTagEvents.length === 0) { if (searchState.tTagEvents.length === 0) {
return createEmptySearchResult("t", searchState.normalizedSearchTerm); return createEmptySearchResult("t", searchState.normalizedSearchTerm);
} }
@ -1287,7 +1368,7 @@ async function processTTagEoseResults(searchState: any, ndk?: NDK): Promise<Sear
searchState.tTagEvents, searchState.tTagEvents,
undefined, // No specific target pubkey for t-tag searches undefined, // No specific target pubkey for t-tag searches
SEARCH_LIMITS.GENERAL_CONTENT, SEARCH_LIMITS.GENERAL_CONTENT,
ndk ndk,
); );
// AI-NOTE: Attach profile data to t-tag events for display // AI-NOTE: Attach profile data to t-tag events for display
@ -1460,7 +1541,9 @@ async function performSecondOrderSearchInBackground(
await Promise.race([fetchPromise, fetchTimeoutPromise]); await Promise.race([fetchPromise, fetchTimeoutPromise]);
// Now do the prioritization without timeout // Now do the prioritization without timeout
console.log("subscription_search: Event fetching completed, starting prioritization..."); console.log(
"subscription_search: Event fetching completed, starting prioritization...",
);
// Deduplicate by event ID // Deduplicate by event ID
const uniqueSecondOrder = new Map<string, NDKEvent>(); const uniqueSecondOrder = new Map<string, NDKEvent>();
@ -1484,18 +1567,18 @@ async function performSecondOrderSearchInBackground(
deduplicatedSecondOrder, deduplicatedSecondOrder,
targetPubkey, targetPubkey,
SEARCH_LIMITS.SECOND_ORDER_RESULTS, SEARCH_LIMITS.SECOND_ORDER_RESULTS,
ndk ndk,
); );
const prioritizationTimeoutPromise = new Promise((_, reject) => { const prioritizationTimeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Prioritization timeout')), 15000); // 15 second timeout setTimeout(() => reject(new Error("Prioritization timeout")), 15000); // 15 second timeout
}); });
let prioritizedSecondOrder: NDKEvent[]; let prioritizedSecondOrder: NDKEvent[];
try { try {
prioritizedSecondOrder = await Promise.race([ prioritizedSecondOrder = await Promise.race([
prioritizationPromise, prioritizationPromise,
prioritizationTimeoutPromise prioritizationTimeoutPromise,
]) as NDKEvent[]; ]) as NDKEvent[];
console.log( console.log(
@ -1504,7 +1587,10 @@ async function performSecondOrderSearchInBackground(
"prioritized results", "prioritized results",
); );
} catch (error) { } catch (error) {
console.warn("subscription_search: Prioritization failed, using simple sorting:", error); console.warn(
"subscription_search: Prioritization failed, using simple sorting:",
error,
);
// Fallback to simple sorting if prioritization fails // Fallback to simple sorting if prioritization fails
prioritizedSecondOrder = deduplicatedSecondOrder.sort((a, b) => { prioritizedSecondOrder = deduplicatedSecondOrder.sort((a, b) => {
// Prioritize events from target pubkey first (for n: searches) // Prioritize events from target pubkey first (for n: searches)
@ -1577,16 +1663,23 @@ async function performSecondOrderSearchInBackground(
* @param ndk NDK instance for fetching profile data * @param ndk NDK instance for fetching profile data
* @returns Promise that resolves when profile data is attached * @returns Promise that resolves when profile data is attached
*/ */
async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<void> { async function attachProfileDataToEvents(
events: NDKEvent[],
ndk: NDK,
): Promise<void> {
if (events.length === 0) { if (events.length === 0) {
return; return;
} }
console.log(`subscription_search: Attaching profile data to ${events.length} events`); console.log(
`subscription_search: Attaching profile data to ${events.length} events`,
);
try { try {
// Import user list functions dynamically to avoid circular dependencies // Import user list functions dynamically to avoid circular dependencies
const { fetchCurrentUserLists, isPubkeyInUserLists } = await import("./user_lists.ts"); const { fetchCurrentUserLists, isPubkeyInUserLists } = await import(
"./user_lists.ts"
);
// Get current user's lists for user list status // Get current user's lists for user list status
const userLists = await fetchCurrentUserLists(undefined, ndk); const userLists = await fetchCurrentUserLists(undefined, ndk);
@ -1599,14 +1692,18 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
} }
}); });
console.log(`subscription_search: Found ${uniquePubkeys.size} unique pubkeys to fetch profiles for`); console.log(
`subscription_search: Found ${uniquePubkeys.size} unique pubkeys to fetch profiles for`,
);
// Fetch profile data for each unique pubkey // Fetch profile data for each unique pubkey
const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => { const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => {
try { try {
// Import getUserMetadata dynamically to avoid circular dependencies // Import getUserMetadata dynamically to avoid circular dependencies
const { getUserMetadata } = await import("./nostrUtils.ts"); const { getUserMetadata } = await import("./nostrUtils.ts");
const npub = await import("./nostrUtils.ts").then(m => m.toNpub(pubkey)); const npub = await import("./nostrUtils.ts").then((m) =>
m.toNpub(pubkey)
);
if (npub) { if (npub) {
const profileData = await getUserMetadata(npub, ndk, true); const profileData = await getUserMetadata(npub, ndk, true);
@ -1619,13 +1716,16 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
pubkey, pubkey,
profileData: { profileData: {
...profileData, ...profileData,
isInUserLists: isInLists isInUserLists: isInLists,
} },
}; };
} }
} }
} catch (error) { } catch (error) {
console.warn(`subscription_search: Failed to fetch profile for ${pubkey}:`, error); console.warn(
`subscription_search: Failed to fetch profile for ${pubkey}:`,
error,
);
} }
return null; return null;
}); });
@ -1640,7 +1740,9 @@ async function attachProfileDataToEvents(events: NDKEvent[], ndk: NDK): Promise<
} }
}); });
console.log(`subscription_search: Successfully fetched ${profileMap.size} profiles`); console.log(
`subscription_search: Successfully fetched ${profileMap.size} profiles`,
);
// Attach profile data to each event // Attach profile data to each event
events.forEach((event) => { events.forEach((event) => {

73
src/lib/utils/user_lists.ts

@ -1,4 +1,4 @@
import { getNdkContext, activeInboxRelays } from "../ndk.ts"; import { activeInboxRelays, getNdkContext } from "../ndk.ts";
import { get } from "svelte/store"; import { get } from "svelte/store";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type NDK from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk";
@ -52,7 +52,7 @@ export interface UserListEvent {
export async function fetchUserLists( export async function fetchUserLists(
pubkey: string, pubkey: string,
listKinds: number[] = [...PEOPLE_LIST_KINDS], listKinds: number[] = [...PEOPLE_LIST_KINDS],
ndk?: NDK ndk?: NDK,
): Promise<UserListEvent[]> { ): Promise<UserListEvent[]> {
const ndkInstance = ndk || getNdkContext(); const ndkInstance = ndk || getNdkContext();
if (!ndkInstance) { if (!ndkInstance) {
@ -60,7 +60,10 @@ export async function fetchUserLists(
return []; return [];
} }
console.log(`fetchUserLists: Fetching lists for ${pubkey}, kinds:`, listKinds); console.log(
`fetchUserLists: Fetching lists for ${pubkey}, kinds:`,
listKinds,
);
try { try {
const events = await ndkInstance.fetchEvents({ const events = await ndkInstance.fetchEvents({
@ -74,8 +77,8 @@ export async function fetchUserLists(
const pubkeys: string[] = []; const pubkeys: string[] = [];
// Extract pubkeys from p-tags // Extract pubkeys from p-tags
event.tags.forEach(tag => { event.tags.forEach((tag) => {
if (tag[0] === 'p' && tag[1]) { if (tag[0] === "p" && tag[1]) {
pubkeys.push(tag[1]); pubkeys.push(tag[1]);
} }
}); });
@ -96,7 +99,7 @@ export async function fetchUserLists(
// Get list name from d-tag if available (for addressable lists) // Get list name from d-tag if available (for addressable lists)
if (!listName && event.kind >= 30000 && event.kind < 40000) { if (!listName && event.kind >= 30000 && event.kind < 40000) {
const dTag = event.getMatchingTags('d')[0]?.[1]; const dTag = event.getMatchingTags("d")[0]?.[1];
if (dTag) { if (dTag) {
listName = dTag; listName = dTag;
} }
@ -111,7 +114,11 @@ export async function fetchUserLists(
}); });
} }
console.log(`fetchUserLists: Found ${userLists.length} lists with ${userLists.reduce((sum, list) => sum + list.pubkeys.length, 0)} total pubkeys`); console.log(
`fetchUserLists: Found ${userLists.length} lists with ${
userLists.reduce((sum, list) => sum + list.pubkeys.length, 0)
} total pubkeys`,
);
return userLists; return userLists;
} catch (error) { } catch (error) {
console.error("fetchUserLists: Error fetching user lists:", error); console.error("fetchUserLists: Error fetching user lists:", error);
@ -127,7 +134,7 @@ export async function fetchUserLists(
*/ */
export async function fetchCurrentUserLists( export async function fetchCurrentUserLists(
listKinds: number[] = [...PEOPLE_LIST_KINDS], listKinds: number[] = [...PEOPLE_LIST_KINDS],
ndk?: NDK ndk?: NDK,
): Promise<UserListEvent[]> { ): Promise<UserListEvent[]> {
const userState = get(userStore); const userState = get(userStore);
@ -145,11 +152,13 @@ export async function fetchCurrentUserLists(
* @param userLists - Array of UserListEvent objects * @param userLists - Array of UserListEvent objects
* @returns Set of unique pubkeys * @returns Set of unique pubkeys
*/ */
export function getPubkeysFromUserLists(userLists: UserListEvent[]): Set<string> { export function getPubkeysFromUserLists(
userLists: UserListEvent[],
): Set<string> {
const pubkeys = new Set<string>(); const pubkeys = new Set<string>();
userLists.forEach(list => { userLists.forEach((list) => {
list.pubkeys.forEach(pubkey => { list.pubkeys.forEach((pubkey) => {
pubkeys.add(pubkey); pubkeys.add(pubkey);
}); });
}); });
@ -163,12 +172,15 @@ export function getPubkeysFromUserLists(userLists: UserListEvent[]): Set<string>
* @param kind - The list kind to filter by * @param kind - The list kind to filter by
* @returns Set of unique pubkeys from the specified list kind * @returns Set of unique pubkeys from the specified list kind
*/ */
export function getPubkeysFromListKind(userLists: UserListEvent[], kind: number): Set<string> { export function getPubkeysFromListKind(
userLists: UserListEvent[],
kind: number,
): Set<string> {
const pubkeys = new Set<string>(); const pubkeys = new Set<string>();
userLists.forEach(list => { userLists.forEach((list) => {
if (list.kind === kind) { if (list.kind === kind) {
list.pubkeys.forEach(pubkey => { list.pubkeys.forEach((pubkey) => {
pubkeys.add(pubkey); pubkeys.add(pubkey);
}); });
} }
@ -183,11 +195,22 @@ export function getPubkeysFromListKind(userLists: UserListEvent[], kind: number)
* @param userLists - Array of UserListEvent objects * @param userLists - Array of UserListEvent objects
* @returns True if the pubkey is in any list * @returns True if the pubkey is in any list
*/ */
export function isPubkeyInUserLists(pubkey: string, userLists: UserListEvent[]): boolean { export function isPubkeyInUserLists(
const result = userLists.some(list => list.pubkeys.includes(pubkey)); pubkey: string,
console.log(`isPubkeyInUserLists: Checking ${pubkey} against ${userLists.length} lists, result: ${result}`); userLists: UserListEvent[],
): boolean {
const result = userLists.some((list) => list.pubkeys.includes(pubkey));
console.log(
`isPubkeyInUserLists: Checking ${pubkey} against ${userLists.length} lists, result: ${result}`,
);
if (result) { if (result) {
console.log(`isPubkeyInUserLists: Found ${pubkey} in lists:`, userLists.filter(list => list.pubkeys.includes(pubkey)).map(list => ({ kind: list.kind, name: list.listName }))); console.log(
`isPubkeyInUserLists: Found ${pubkey} in lists:`,
userLists.filter((list) => list.pubkeys.includes(pubkey)).map((list) => ({
kind: list.kind,
name: list.listName,
})),
);
} }
return result; return result;
} }
@ -198,10 +221,13 @@ export function isPubkeyInUserLists(pubkey: string, userLists: UserListEvent[]):
* @param userLists - Array of UserListEvent objects * @param userLists - Array of UserListEvent objects
* @returns Array of list kinds that contain the pubkey * @returns Array of list kinds that contain the pubkey
*/ */
export function getListKindsForPubkey(pubkey: string, userLists: UserListEvent[]): number[] { export function getListKindsForPubkey(
pubkey: string,
userLists: UserListEvent[],
): number[] {
return userLists return userLists
.filter(list => list.pubkeys.includes(pubkey)) .filter((list) => list.pubkeys.includes(pubkey))
.map(list => list.kind); .map((list) => list.kind);
} }
/** /**
@ -209,7 +235,10 @@ export function getListKindsForPubkey(pubkey: string, userLists: UserListEvent[]
* This ensures follows are always cached and prioritized * This ensures follows are always cached and prioritized
* @param pubkeys - Array of pubkeys to cache profiles for * @param pubkeys - Array of pubkeys to cache profiles for
*/ */
export async function updateProfileCacheForPubkeys(pubkeys: string[], ndk?: NDK): Promise<void> { export async function updateProfileCacheForPubkeys(
pubkeys: string[],
ndk?: NDK,
): Promise<void> {
if (pubkeys.length === 0) return; if (pubkeys.length === 0) return;
try { try {

1
src/routes/publication/[type]/[identifier]/+layout.server.ts

@ -1,6 +1,5 @@
import type { LayoutServerLoad } from "./$types"; import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = ({ url }: { url: URL }) => { export const load: LayoutServerLoad = ({ url }: { url: URL }) => {
const currentUrl = `${url.origin}${url.pathname}`; const currentUrl = `${url.origin}${url.pathname}`;

3
src/routes/publication/[type]/[identifier]/+page.ts

@ -49,7 +49,8 @@ export const load: PageLoad = async (
// AI-NOTE: Return null for indexEvent during SSR or when fetch fails // AI-NOTE: Return null for indexEvent during SSR or when fetch fails
// The component will handle client-side loading and error states // The component will handle client-side loading and error states
const publicationType = indexEvent?.tags.find((tag) => tag[0] === "type")?.[1] ?? ""; const publicationType =
indexEvent?.tags.find((tag) => tag[0] === "type")?.[1] ?? "";
const result = { const result = {
publicationType, publicationType,

8
src/styles/notifications.css

@ -107,15 +107,11 @@
} }
.message-container:hover { .message-container:hover {
box-shadow: box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
} }
.dark .message-container:hover { .dark .message-container:hover {
box-shadow: box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2);
0 4px 6px -1px rgb(0 0 0 / 0.3),
0 2px 4px -2px rgb(0 0 0 / 0.2);
} }
/* Filter indicator styling */ /* Filter indicator styling */

3
src/styles/scrollbar.css

@ -1,7 +1,8 @@
@layer components { @layer components {
/* Global scrollbar styles */ /* Global scrollbar styles */
* { * {
scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ scrollbar-color: rgba(87, 66, 41, 0.8)
transparent; /* Transparent track, default scrollbar thumb */
} }
/* Webkit Browsers (Chrome, Safari, Edge) */ /* Webkit Browsers (Chrome, Safari, Edge) */

71
tests/unit/mathProcessing.test.ts

@ -3,29 +3,38 @@ import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupPa
describe("Math Processing in Advanced Markup Parser", () => { describe("Math Processing in Advanced Markup Parser", () => {
it("should process inline math inside code blocks", async () => { it("should process inline math inside code blocks", async () => {
const input = "Here is some inline math: `$x^2 + y^2 = z^2$` in a sentence."; const input =
"Here is some inline math: `$x^2 + y^2 = z^2$` in a sentence.";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(x^2 + y^2 = z^2\\)</span>'); expect(result).toContain(
'<span class="math-inline">\\(x^2 + y^2 = z^2\\)</span>',
);
expect(result).toContain("Here is some inline math:"); expect(result).toContain("Here is some inline math:");
expect(result).toContain("in a sentence."); expect(result).toContain("in a sentence.");
}); });
it("should process display math inside code blocks", async () => { it("should process display math inside code blocks", async () => {
const input = "Here is a display equation:\n\n`$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$`\n\nThis is after the equation."; const input =
"Here is a display equation:\n\n`$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$`\n\nThis is after the equation.";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n\\]</span>'); expect(result).toContain(
'<span class="math-display">\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n\\]</span>',
);
expect(result).toContain('<p class="my-4">Here is a display equation:</p>'); expect(result).toContain('<p class="my-4">Here is a display equation:</p>');
expect(result).toContain('<p class="my-4">This is after the equation.</p>'); expect(result).toContain('<p class="my-4">This is after the equation.</p>');
}); });
it("should process both inline and display math in the same code block", async () => { it("should process both inline and display math in the same code block", async () => {
const input = "Mixed math: `$\\alpha$ and $$\\beta = \\frac{1}{2}$$` in one block."; const input =
"Mixed math: `$\\alpha$ and $$\\beta = \\frac{1}{2}$$` in one block.";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(\\alpha\\)</span>'); expect(result).toContain('<span class="math-inline">\\(\\alpha\\)</span>');
expect(result).toContain('<span class="math-display">\\[\\beta = \\frac{1}{2}\\]</span>'); expect(result).toContain(
'<span class="math-display">\\[\\beta = \\frac{1}{2}\\]</span>',
);
expect(result).toContain("Mixed math:"); expect(result).toContain("Mixed math:");
expect(result).toContain("in one block."); expect(result).toContain("in one block.");
}); });
@ -40,16 +49,20 @@ describe("Math Processing in Advanced Markup Parser", () => {
}); });
it("should NOT process display math outside of code blocks", async () => { it("should NOT process display math outside of code blocks", async () => {
const input = "This display math $$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$ should not be processed."; const input =
"This display math $$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$ should not be processed.";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain("$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$"); expect(result).toContain(
"$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$",
);
expect(result).not.toContain('<span class="math-inline">'); expect(result).not.toContain('<span class="math-inline">');
expect(result).not.toContain('<span class="math-display">'); expect(result).not.toContain('<span class="math-display">');
}); });
it("should handle code blocks without math normally", async () => { it("should handle code blocks without math normally", async () => {
const input = "Here is some code: `console.log('hello world')` that should not be processed."; const input =
"Here is some code: `console.log('hello world')` that should not be processed.";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain("`console.log('hello world')`"); expect(result).toContain("`console.log('hello world')`");
@ -58,7 +71,8 @@ describe("Math Processing in Advanced Markup Parser", () => {
}); });
it("should handle complex math expressions with nested structures", async () => { it("should handle complex math expressions with nested structures", async () => {
const input = "Complex math: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\cdot \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax + by \\\\ cx + dy \\end{pmatrix}$$`"; const input =
"Complex math: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\cdot \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax + by \\\\ cx + dy \\end{pmatrix}$$`";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">'); expect(result).toContain('<span class="math-display">');
@ -68,23 +82,32 @@ describe("Math Processing in Advanced Markup Parser", () => {
}); });
it("should handle inline math with special characters", async () => { it("should handle inline math with special characters", async () => {
const input = "Special chars: `$\\alpha, \\beta, \\gamma, \\delta$` and `$\\sum_{i=1}^{n} x_i$`"; const input =
"Special chars: `$\\alpha, \\beta, \\gamma, \\delta$` and `$\\sum_{i=1}^{n} x_i$`";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(\\alpha, \\beta, \\gamma, \\delta\\)</span>'); expect(result).toContain(
expect(result).toContain('<span class="math-inline">\\(\\sum_{i=1}^{n} x_i\\)</span>'); '<span class="math-inline">\\(\\alpha, \\beta, \\gamma, \\delta\\)</span>',
);
expect(result).toContain(
'<span class="math-inline">\\(\\sum_{i=1}^{n} x_i\\)</span>',
);
}); });
it("should handle multiple math expressions in separate code blocks", async () => { it("should handle multiple math expressions in separate code blocks", async () => {
const input = "First: `$E = mc^2$` and second: `$$F = G\\frac{m_1 m_2}{r^2}$$`"; const input =
"First: `$E = mc^2$` and second: `$$F = G\\frac{m_1 m_2}{r^2}$$`";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(E = mc^2\\)</span>'); expect(result).toContain('<span class="math-inline">\\(E = mc^2\\)</span>');
expect(result).toContain('<span class="math-display">\\[F = G\\frac{m_1 m_2}{r^2}\\]</span>'); expect(result).toContain(
'<span class="math-display">\\[F = G\\frac{m_1 m_2}{r^2}\\]</span>',
);
}); });
it("should handle math expressions with line breaks in display mode", async () => { it("should handle math expressions with line breaks in display mode", async () => {
const input = "Multi-line: `$$\n\\begin{align}\nx &= a + b \\\\\ny &= c + d\n\\end{align}\n$$`"; const input =
"Multi-line: `$$\n\\begin{align}\nx &= a + b \\\\\ny &= c + d\n\\end{align}\n$$`";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">'); expect(result).toContain('<span class="math-display">');
@ -124,7 +147,9 @@ And more regular text.`;
expect(result).toContain("`console.log('hello')`"); expect(result).toContain("`console.log('hello')`");
// Should process math // Should process math
expect(result).toContain('<span class="math-inline">\\(\\pi \\approx 3.14159\\)</span>'); expect(result).toContain(
'<span class="math-inline">\\(\\pi \\approx 3.14159\\)</span>',
);
expect(result).toContain('<span class="math-display">'); expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\int_0^1 x^2 dx = \\frac{1}{3}"); expect(result).toContain("\\int_0^1 x^2 dx = \\frac{1}{3}");
}); });
@ -138,7 +163,8 @@ And more regular text.`;
}); });
it("should handle display math with dollar signs in the content", async () => { it("should handle display math with dollar signs in the content", async () => {
const input = "Price display: `$$\n\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98\n$$`"; const input =
"Price display: `$$\n\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98\n$$`";
const result = await parseAdvancedmarkup(input); const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">'); expect(result).toContain('<span class="math-display">');
@ -156,7 +182,8 @@ And more regular text.`;
it("should handle JSON content with escaped display math", async () => { it("should handle JSON content with escaped display math", async () => {
// Simulate content from JSON where backslashes are escaped // Simulate content from JSON where backslashes are escaped
const jsonContent = "Display math from JSON: `$$\\\\int_0^1 x^2 dx = \\\\frac{1}{3}$$`"; const jsonContent =
"Display math from JSON: `$$\\\\int_0^1 x^2 dx = \\\\frac{1}{3}$$`";
const result = await parseAdvancedmarkup(jsonContent); const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-display">'); expect(result).toContain('<span class="math-display">');
@ -165,7 +192,8 @@ And more regular text.`;
it("should handle JSON content with escaped dollar signs", async () => { it("should handle JSON content with escaped dollar signs", async () => {
// Simulate content from JSON where dollar signs are escaped // Simulate content from JSON where dollar signs are escaped
const jsonContent = "Price math from JSON: `$\\\\text{Price} = \\\\\\$19.99$`"; const jsonContent =
"Price math from JSON: `$\\\\text{Price} = \\\\\\$19.99$`";
const result = await parseAdvancedmarkup(jsonContent); const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-inline">'); expect(result).toContain('<span class="math-inline">');
@ -174,7 +202,8 @@ And more regular text.`;
it("should handle complex JSON content with multiple escaped characters", async () => { it("should handle complex JSON content with multiple escaped characters", async () => {
// Simulate complex content from JSON // Simulate complex content from JSON
const jsonContent = "Complex JSON math: `$$\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix} \\\\cdot \\\\begin{pmatrix} x \\\\\\\\ y \\\\end{pmatrix}$$`"; const jsonContent =
"Complex JSON math: `$$\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix} \\\\cdot \\\\begin{pmatrix} x \\\\\\\\ y \\\\end{pmatrix}$$`";
const result = await parseAdvancedmarkup(jsonContent); const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-display">'); expect(result).toContain('<span class="math-display">');

8
tests/unit/tagExpansion.test.ts

@ -335,7 +335,9 @@ describe("Tag Expansion Tests", () => {
); );
// Should not include events without tags // Should not include events without tags
expect(result.publications.map((p: any) => p.id)).not.toContain("no-tags"); expect(result.publications.map((p: any) => p.id)).not.toContain(
"no-tags",
);
}); });
}); });
@ -512,7 +514,9 @@ describe("Tag Expansion Tests", () => {
// Should handle d-tags with colons correctly // Should handle d-tags with colons correctly
expect(result.publications).toHaveLength(3); expect(result.publications).toHaveLength(3);
expect(result.contentEvents.map((c: any) => c.id)).toContain("colon-content"); expect(result.contentEvents.map((c: any) => c.id)).toContain(
"colon-content",
);
}); });
}); });
}); });

Loading…
Cancel
Save