Browse Source

Consolidate some state values in search

master
buttercat1791 7 months ago
parent
commit
8b61566322
  1. 131
      src/lib/components/EventSearch.svelte
  2. 1
      src/lib/models/search_type.d.ts
  3. 367
      src/routes/events/+page.svelte

131
src/lib/components/EventSearch.svelte

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from "$app/navigation";
import { Input, Button } from "flowbite-svelte"; import { Input, Button } from "flowbite-svelte";
import { Spinner } from "flowbite-svelte"; import { Spinner } from "flowbite-svelte";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
@ -8,19 +8,22 @@
searchBySubscription, searchBySubscription,
searchNip05, searchNip05,
} from "$lib/utils/search_utility"; } from "$lib/utils/search_utility";
import type { SearchCallbacks } from "$lib/utils/search_types";
import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils"; import { neventEncode, naddrEncode, nprofileEncode } from "$lib/utils";
import { activeInboxRelays, activeOutboxRelays, getNdkContext } from "$lib/ndk"; import {
activeInboxRelays,
activeOutboxRelays,
getNdkContext,
} from "$lib/ndk";
import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils";
import { isEventId } from "$lib/utils/nostr_identifiers"; import { isEventId } from "$lib/utils/nostr_identifiers";
import type NDK from '@nostr-dev-kit/ndk'; import type { SearchType } from "$lib/models/search_type";
// Props definition // Props definition
let { let {
loading, loading,
error, error,
searchValue, searchValue,
dTagValue, searchType,
onEventFound, onEventFound,
onSearchResults, onSearchResults,
event, event,
@ -30,7 +33,7 @@
loading: boolean; loading: boolean;
error: string | null; error: string | null;
searchValue: string | null; searchValue: string | null;
dTagValue: string | null; searchType: SearchType | null;
onEventFound: (event: NDKEvent) => void; onEventFound: (event: NDKEvent) => void;
onSearchResults: ( onSearchResults: (
firstOrder: NDKEvent[], firstOrder: NDKEvent[],
@ -70,7 +73,7 @@
// Track last processed values to prevent loops // Track last processed values to prevent loops
let lastProcessedSearchValue = $state<string | null>(null); let lastProcessedSearchValue = $state<string | null>(null);
let lastProcessedDTagValue = $state<string | null>(null); let lastProcessedSearchType = $state<SearchType | null>(null);
let isProcessingSearch = $state(false); let isProcessingSearch = $state(false);
let currentProcessingSearchValue = $state<string | null>(null); let currentProcessingSearchValue = $state<string | null>(null);
let lastSearchValue = $state<string | null>(null); let lastSearchValue = $state<string | null>(null);
@ -110,7 +113,10 @@
updateSearchState(false, true, 1, "event"); updateSearchState(false, true, 1, "event");
} }
} catch (err) { } catch (err) {
handleSearchError(err, "Error fetching event. Please check the ID and try again."); handleSearchError(
err,
"Error fetching event. Please check the ID and try again.",
);
} }
} }
@ -129,7 +135,9 @@
isResetting = false; isResetting = false;
isUserEditing = false; isUserEditing = false;
const query = (queryOverride !== undefined ? queryOverride || "" : searchQuery || "").trim(); const query = (
queryOverride !== undefined ? queryOverride || "" : searchQuery || ""
).trim();
if (!query) { if (!query) {
updateSearchState(false, false, null, null); updateSearchState(false, false, null, null);
return; return;
@ -181,7 +189,12 @@
// AI-NOTE: 2025-01-24 - Treat plain text searches as profile searches by default // AI-NOTE: 2025-01-24 - Treat plain text searches as profile searches by default
// This allows searching for names like "thebeave" or "TheBeave" without needing n: prefix // This allows searching for names like "thebeave" or "TheBeave" without needing n: prefix
if (trimmedQuery && !trimmedQuery.startsWith("nevent") && !trimmedQuery.startsWith("npub") && !trimmedQuery.startsWith("naddr")) { if (
trimmedQuery &&
!trimmedQuery.startsWith("nevent") &&
!trimmedQuery.startsWith("npub") &&
!trimmedQuery.startsWith("naddr")
) {
return { type: "n", term: trimmedQuery }; return { type: "n", term: trimmedQuery };
} }
@ -191,7 +204,7 @@
async function handleSearchByType( async function handleSearchByType(
searchType: { type: string; term: string }, searchType: { type: string; term: string },
query: string, query: string,
clearInput: boolean clearInput: boolean,
) { ) {
const { type, term } = searchType; const { type, term } = searchType;
@ -250,10 +263,16 @@
return; return;
} }
if (dTagValue) { if (searchValue && searchType) {
searchQuery = `d:${dTagValue}`; if (searchType === "d") {
} else if (searchValue) { searchQuery = `d:${searchValue}`;
} else if (searchType === "t") {
searchQuery = `t:${searchValue}`;
} else if (searchType === "n") {
searchQuery = `n:${searchValue}`;
} else {
searchQuery = searchValue; searchQuery = searchValue;
}
} else if (!searchQuery) { } else if (!searchQuery) {
searchQuery = ""; searchQuery = "";
} }
@ -303,17 +322,29 @@
$effect(() => { $effect(() => {
if ( if (
dTagValue && searchValue &&
searchType &&
!searching && !searching &&
!isResetting && !isResetting &&
dTagValue !== lastProcessedDTagValue (searchType !== lastProcessedSearchType ||
searchValue !== lastProcessedSearchValue)
) { ) {
console.log("EventSearch: Processing dTagValue:", dTagValue); console.log("EventSearch: Processing search:", {
lastProcessedDTagValue = dTagValue; searchType,
searchValue,
});
lastProcessedSearchType = searchType;
lastProcessedSearchValue = searchValue;
setTimeout(() => { setTimeout(() => {
if (!searching && !isResetting) { if (!searching && !isResetting) {
handleSearchBySubscription("d", dTagValue); if (searchType === "d") {
handleSearchBySubscription("d", searchValue);
} else if (searchType === "t") {
handleSearchBySubscription("t", searchValue);
} else if (searchType === "n") {
handleSearchBySubscription("n", searchValue);
}
} }
}, 100); }, 100);
} }
@ -386,7 +417,7 @@
foundEvent = null; foundEvent = null;
localError = null; localError = null;
lastProcessedSearchValue = null; lastProcessedSearchValue = null;
lastProcessedDTagValue = null; lastProcessedSearchType = null;
isProcessingSearch = false; isProcessingSearch = false;
currentProcessingSearchValue = null; currentProcessingSearchValue = null;
lastSearchValue = null; lastSearchValue = null;
@ -421,6 +452,10 @@
lastSearchValue = searchValue; lastSearchValue = searchValue;
} }
if (searchType) {
lastProcessedSearchType = searchType;
}
isProcessingSearch = false; isProcessingSearch = false;
currentProcessingSearchValue = null; currentProcessingSearchValue = null;
isWaitingForSearchResult = false; isWaitingForSearchResult = false;
@ -476,18 +511,10 @@
while (retryCount < maxRetries) { while (retryCount < maxRetries) {
// Check if we have any relays in the NDK pool // Check if we have any relays in the NDK pool
if (ndk && ndk.pool && ndk.pool.relays && ndk.pool.relays.size > 0) { if (ndk && ndk.pool && ndk.pool.relays && ndk.pool.relays.size > 0) {
console.debug(`EventSearch: Found ${ndk.pool.relays.size} relays in NDK pool`);
break;
}
// Also check active relay stores as fallback
if ($activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0) {
console.debug(`EventSearch: Found active relays - inbox: ${$activeInboxRelays.length}, outbox: ${$activeOutboxRelays.length}`);
break; break;
} }
console.debug(`EventSearch: Waiting for relays... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise((resolve) => setTimeout(resolve, 500));
await new Promise(resolve => setTimeout(resolve, 500));
retryCount++; retryCount++;
} }
@ -499,18 +526,30 @@
poolRelayCount, poolRelayCount,
inboxCount: $activeInboxRelays.length, inboxCount: $activeInboxRelays.length,
outboxCount: $activeOutboxRelays.length, outboxCount: $activeOutboxRelays.length,
willUseAllRelays: poolRelayCount > 0 || $activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0 willUseAllRelays:
poolRelayCount > 0 ||
$activeInboxRelays.length > 0 ||
$activeOutboxRelays.length > 0,
}); });
// If we have any relays available, proceed with search // If we have any relays available, proceed with search
if (poolRelayCount > 0 || $activeInboxRelays.length > 0 || $activeOutboxRelays.length > 0) { if (
poolRelayCount > 0 ||
$activeInboxRelays.length > 0 ||
$activeOutboxRelays.length > 0
) {
console.log("EventSearch: Relays available, proceeding with search"); console.log("EventSearch: Relays available, proceeding with search");
} else { } else {
console.warn("EventSearch: No relays detected, but proceeding with search - fallback relays will be used"); console.warn(
"EventSearch: No relays detected, but proceeding with search - fallback relays will be used",
);
} }
} }
async function performSubscriptionSearch(searchType: "d" | "t" | "n", searchTerm: string): Promise<void> { async function performSubscriptionSearch(
searchType: "d" | "t" | "n",
searchTerm: string,
): Promise<void> {
if (currentAbortController) { if (currentAbortController) {
currentAbortController.abort(); currentAbortController.abort();
} }
@ -547,11 +586,13 @@
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => { setTimeout(() => {
reject(new Error("Search timeout: No results received within 30 seconds")); reject(
new Error("Search timeout: No results received within 30 seconds"),
);
}, 30000); }, 30000);
}); });
const result = await Promise.race([searchPromise, timeoutPromise]) as any; const result = (await Promise.race([searchPromise, timeoutPromise])) as any;
console.log("EventSearch: Search completed:", result); console.log("EventSearch: Search completed:", result);
onSearchResults( onSearchResults(
@ -565,7 +606,10 @@
false, // AI-NOTE: 2025-01-24 - Search is complete false, // AI-NOTE: 2025-01-24 - Search is complete
); );
const totalCount = result.events.length + result.secondOrder.length + result.tTagEvents.length; const totalCount =
result.events.length +
result.secondOrder.length +
result.tTagEvents.length;
localError = null; localError = null;
cleanupSearch(); cleanupSearch();
@ -590,10 +634,15 @@
console.error("EventSearch: Search failed:", error); console.error("EventSearch: Search failed:", error);
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes("timeout") || error.message.includes("connection")) { if (
localError = "Search timed out. The relays may be temporarily unavailable. Please try again."; error.message.includes("timeout") ||
error.message.includes("connection")
) {
localError =
"Search timed out. The relays may be temporarily unavailable. Please try again.";
} else if (error.message.includes("NDK not initialized")) { } else if (error.message.includes("NDK not initialized")) {
localError = "Nostr client not initialized. Please refresh the page and try again."; localError =
"Nostr client not initialized. Please refresh the page and try again.";
} else { } else {
localError = `Search failed: ${error.message}`; localError = `Search failed: ${error.message}`;
} }
@ -610,6 +659,10 @@
if (searchValue) { if (searchValue) {
lastProcessedSearchValue = searchValue; lastProcessedSearchValue = searchValue;
} }
if (searchType) {
lastProcessedSearchType = searchType;
}
} }
// AI-NOTE: 2025-01-24 - Background profile search is now handled by centralized searchProfiles function // AI-NOTE: 2025-01-24 - Background profile search is now handled by centralized searchProfiles function

1
src/lib/models/search_type.d.ts vendored

@ -0,0 +1 @@
export type SearchType = "id" | "d" | "t" | "n";

367
src/routes/events/+page.svelte

@ -9,7 +9,11 @@
import CommentBox from "$lib/components/CommentBox.svelte"; import CommentBox from "$lib/components/CommentBox.svelte";
import CommentViewer from "$lib/components/CommentViewer.svelte"; import CommentViewer from "$lib/components/CommentViewer.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { getMatchingTags, toNpub, getUserMetadata } from "$lib/utils/nostrUtils"; import {
getMatchingTags,
toNpub,
getUserMetadata,
} from "$lib/utils/nostrUtils";
import EventInput from "$lib/components/EventInput.svelte"; import EventInput from "$lib/components/EventInput.svelte";
import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte";
import { neventEncode, naddrEncode } from "$lib/utils"; import { neventEncode, naddrEncode } from "$lib/utils";
@ -19,21 +23,24 @@
import { checkCommunity } from "$lib/utils/search_utility"; import { checkCommunity } from "$lib/utils/search_utility";
import EmbeddedEvent from "$lib/components/embedded_events/EmbeddedEvent.svelte"; import EmbeddedEvent from "$lib/components/embedded_events/EmbeddedEvent.svelte";
import { userStore } from "$lib/stores/userStore"; import { userStore } from "$lib/stores/userStore";
import { fetchCurrentUserLists, isPubkeyInUserLists } from "$lib/utils/user_lists"; import {
fetchCurrentUserLists,
isPubkeyInUserLists,
} from "$lib/utils/user_lists";
import { UserOutline } from "flowbite-svelte-icons"; import { UserOutline } from "flowbite-svelte-icons";
import type { UserProfile } from "$lib/models/user_profile"; import type { UserProfile } from "$lib/models/user_profile";
import type { SearchType } from "$lib/models/search_type";
let loading = $state(false); let loading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let searchValue = $state<string | null>(null); let searchValue = $state<string | null>(null);
let dTagValue = $state<string | null>(null); let searchType = $state<SearchType | null>(null);
let event = $state<NDKEvent | null>(null); let event = $state<NDKEvent | null>(null);
let searchResults = $state<NDKEvent[]>([]); let searchResults = $state<NDKEvent[]>([]);
let secondOrderResults = $state<NDKEvent[]>([]); let secondOrderResults = $state<NDKEvent[]>([]);
let tTagResults = $state<NDKEvent[]>([]); let tTagResults = $state<NDKEvent[]>([]);
let originalEventIds = $state<Set<string>>(new Set()); let originalEventIds = $state<Set<string>>(new Set());
let originalAddresses = $state<Set<string>>(new Set()); let originalAddresses = $state<Set<string>>(new Set());
let searchType = $state<string | null>(null);
let searchTerm = $state<string | null>(null); let searchTerm = $state<string | null>(null);
let profile = $state<UserProfile | null>(null); let profile = $state<UserProfile | null>(null);
let userRelayPreference = $state(false); let userRelayPreference = $state(false);
@ -61,25 +68,36 @@
const parsedProfile = parseProfileContent(newEvent); const parsedProfile = parseProfileContent(newEvent);
if (parsedProfile) { if (parsedProfile) {
profile = parsedProfile; profile = parsedProfile;
console.log("[Events Page] Parsed profile data:", parsedProfile);
// If the event doesn't have user list information, fetch it // If the event doesn't have user list information, fetch it
if (typeof parsedProfile.isInUserLists !== 'boolean') { if (typeof parsedProfile.isInUserLists !== "boolean") {
fetchCurrentUserLists(undefined, ndk) fetchCurrentUserLists(undefined, ndk)
.then((userLists) => { .then((userLists) => {
const isInLists = isPubkeyInUserLists(newEvent.pubkey, userLists); const isInLists = isPubkeyInUserLists(
newEvent.pubkey,
userLists,
);
// Update the profile with user list information // Update the profile with user list information
profile = { ...parsedProfile, isInUserLists: isInLists } as any; profile = { ...parsedProfile, isInUserLists: isInLists } as any;
// Also update the event's profileData // Also update the event's profileData
(newEvent as any).profileData = { ...parsedProfile, isInUserLists: isInLists }; (newEvent as any).profileData = {
...parsedProfile,
isInUserLists: isInLists,
};
}) })
.catch(() => { .catch(() => {
profile = { ...parsedProfile, isInUserLists: false } as any; profile = { ...parsedProfile, isInUserLists: false } as any;
(newEvent as any).profileData = { ...parsedProfile, isInUserLists: false }; (newEvent as any).profileData = {
...parsedProfile,
isInUserLists: false,
};
}); });
} }
} else { } else {
console.warn("[Events Page] Failed to parse profile content for event:", newEvent.id); console.warn(
"[Events Page] Failed to parse profile content for event:",
newEvent.id,
);
profile = null; profile = null;
} }
} catch (error) { } catch (error) {
@ -120,7 +138,10 @@
console.log(`[Events Page] Cached profile for pubkey: ${pubkey}`); console.log(`[Events Page] Cached profile for pubkey: ${pubkey}`);
} }
} catch (error) { } catch (error) {
console.warn(`[Events Page] Failed to cache profile for ${pubkey}:`, error); console.warn(
`[Events Page] Failed to cache profile for ${pubkey}:`,
error,
);
} }
} }
@ -131,66 +152,60 @@
for (const event of events) { for (const event of events) {
if (event.kind === 0 && event.pubkey) { if (event.kind === 0 && event.pubkey) {
const existingProfileData = (event as any).profileData || parseProfileContent(event); const existingProfileData =
(event as any).profileData || parseProfileContent(event);
if (existingProfileData) { if (existingProfileData) {
const isInLists = isPubkeyInUserLists(event.pubkey, userLists); const isInLists = isPubkeyInUserLists(event.pubkey, userLists);
(event as any).profileData = { ...existingProfileData, isInUserLists: isInLists }; (event as any).profileData = {
...existingProfileData,
isInUserLists: isInLists,
};
} }
} }
} }
} catch (error) { } catch (error) {
console.warn("[Events Page] Failed to update profile data with user lists:", error); console.warn(
"[Events Page] Failed to update profile data with user lists:",
error,
);
} }
} }
// Use Svelte 5 idiomatic effect to update searchValue when $page.url.searchParams.get('id') changes // Use Svelte 5 idiomatic effect to update searchValue and searchType based on URL parameters
$effect(() => { $effect(() => {
const url = $page.url.searchParams; const url = $page.url.searchParams;
const idParam = url.get("id"); const idParam = url.get("id");
const dParam = url.get("d"); const dParam = url.get("d");
const tParam = url.get("t");
const nParam = url.get("n");
if (idParam) { if (idParam) {
searchValue = idParam; searchValue = idParam;
dTagValue = null; searchType = "id";
} else if (dParam) { } else if (dParam) {
searchValue = null; searchValue = dParam.toLowerCase();
dTagValue = dParam.toLowerCase(); searchType = "d";
} else if (tParam) {
searchValue = decodeURIComponent(tParam);
searchType = "t";
} else if (nParam) {
searchValue = decodeURIComponent(nParam);
searchType = "n";
} else { } else {
searchValue = null; searchValue = null;
dTagValue = null; searchType = null;
}
});
// Add support for t and n parameters
$effect(() => {
const url = $page.url.searchParams;
const tParam = url.get("t");
const nParam = url.get("n");
if (tParam) {
// Decode the t parameter and set it as searchValue with t: prefix
const decodedT = decodeURIComponent(tParam);
searchValue = `t:${decodedT}`;
dTagValue = null;
} else if (nParam) {
// Decode the n parameter and set it as searchValue with n: prefix
const decodedN = decodeURIComponent(nParam);
searchValue = `n:${decodedN}`;
dTagValue = null;
} }
}); });
// Handle side panel visibility based on search type // Handle side panel visibility based on search type
$effect(() => { $effect(() => {
const url = $page.url.searchParams; // Close side panel for searches that return multiple results (d-tag, t-tag, name searches)
const hasIdParam = url.get("id"); if (
const hasDParam = url.get("d"); searchType === "d" ||
const hasTParam = url.get("t"); searchType === "t" ||
const hasNParam = url.get("n"); searchType === "n"
) {
// Close side panel for searches that return multiple results
if (hasDParam || hasTParam || hasNParam) {
showSidePanel = false; showSidePanel = false;
event = null; event = null;
profile = null; profile = null;
@ -200,11 +215,14 @@
// AI-NOTE: 2025-01-24 - Function to ensure events have created_at property // AI-NOTE: 2025-01-24 - Function to ensure events have created_at property
// This fixes the "Unknown date" issue when events are retrieved from cache // This fixes the "Unknown date" issue when events are retrieved from cache
function ensureEventProperties(events: NDKEvent[]): NDKEvent[] { function ensureEventProperties(events: NDKEvent[]): NDKEvent[] {
return events.map(event => { return events.map((event) => {
if (event && typeof event === 'object') { if (event && typeof event === "object") {
// Ensure created_at is set // Ensure created_at is set
if (!event.created_at && event.created_at !== 0) { if (!event.created_at && event.created_at !== 0) {
console.warn("[Events Page] Event missing created_at, setting to 0:", event.id); console.warn(
"[Events Page] Event missing created_at, setting to 0:",
event.id,
);
(event as any).created_at = 0; (event as any).created_at = 0;
} }
} }
@ -231,7 +249,7 @@
tTagResults = processedTTagEvents; tTagResults = processedTTagEvents;
originalEventIds = eventIds; originalEventIds = eventIds;
originalAddresses = addresses; originalAddresses = addresses;
searchType = searchTypeParam || null; searchType = searchTypeParam as SearchType | null;
searchTerm = searchTermParam || null; searchTerm = searchTermParam || null;
// Track search progress // Track search progress
@ -279,16 +297,16 @@
// AI-NOTE: 2025-01-24 - Function to cache profiles for multiple events // AI-NOTE: 2025-01-24 - Function to cache profiles for multiple events
async function cacheProfilesForEvents(events: NDKEvent[]) { async function cacheProfilesForEvents(events: NDKEvent[]) {
const uniquePubkeys = new Set<string>(); const uniquePubkeys = new Set<string>();
events.forEach(event => { events.forEach((event) => {
if (event.pubkey) { if (event.pubkey) {
uniquePubkeys.add(event.pubkey); uniquePubkeys.add(event.pubkey);
} }
}); });
console.log(`[Events Page] Caching profiles for ${uniquePubkeys.size} unique pubkeys`);
// Cache profiles in parallel // Cache profiles in parallel
const cachePromises = Array.from(uniquePubkeys).map(pubkey => cacheProfileForPubkey(pubkey)); const cachePromises = Array.from(uniquePubkeys).map((pubkey) =>
cacheProfileForPubkey(pubkey),
);
await Promise.allSettled(cachePromises); await Promise.allSettled(cachePromises);
// AI-NOTE: 2025-01-24 - Update profile data with user list information for cached events // AI-NOTE: 2025-01-24 - Update profile data with user list information for cached events
@ -447,13 +465,18 @@
communityStatus = { ...communityStatus, ...newCommunityStatus }; communityStatus = { ...communityStatus, ...newCommunityStatus };
} }
</script> </script>
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
<div class="flex flex-col lg:flex-row w-full max-w-7xl my-6 px-4 mx-auto gap-6"> <div
class="flex flex-col lg:flex-row w-full max-w-7xl my-6 px-4 mx-auto gap-6"
>
<!-- Left Panel: Search and Results --> <!-- Left Panel: Search and Results -->
<div class={showSidePanel ? "w-full lg:w-80 lg:min-w-80" : "flex-1 max-w-4xl mx-auto"}> <div
class={showSidePanel
? "w-full lg:w-80 lg:min-w-80"
: "flex-1 max-w-4xl mx-auto"}
>
<div class="main-leather flex flex-col space-y-6"> <div class="main-leather flex flex-col space-y-6">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<Heading tag="h1" class="h-leather mb-2">Events</Heading> <Heading tag="h1" class="h-leather mb-2">Events</Heading>
@ -480,22 +503,37 @@
<P class="mb-3"> <P class="mb-3">
Search and explore Nostr events across the network. Find events by: Search and explore Nostr events across the network. Find events by:
</P> </P>
<ul class="mb-3 list-disc list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300"> <ul
<li><strong>Event identifiers:</strong> nevent, note, naddr, npub, nprofile, pubkey, or event ID</li> class="mb-3 list-disc list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300"
>
<li>
<strong>Event identifiers:</strong> nevent, note, naddr, npub, nprofile,
pubkey, or event ID
</li>
<li><strong>NIP-05 addresses:</strong> username@domain.com</li> <li><strong>NIP-05 addresses:</strong> username@domain.com</li>
<li><strong>Profile names:</strong> Search by display name or username (use "n:" prefix for exact matches)</li> <li>
<li><strong>D-tags:</strong> Find events with specific d-tags using "d:tag-name"</li> <strong>Profile names:</strong> Search by display name or username (use
<li><strong>T-tags:</strong> Find events tagged with specific topics using "t:topic"</li> "n:" prefix for exact matches)
</li>
<li>
<strong>D-tags:</strong> Find events with specific d-tags using "d:tag-name"
</li>
<li>
<strong>T-tags:</strong> Find events tagged with specific topics using
"t:topic"
</li>
</ul> </ul>
<P class="mb-3 text-sm text-gray-600 dark:text-gray-400"> <P class="mb-3 text-sm text-gray-600 dark:text-gray-400">
The page shows primary search results, second-order references (replies, quotes, mentions), and related tagged events. Click any event to view details, comments, and relay information. The page shows primary search results, second-order references
(replies, quotes, mentions), and related tagged events. Click any
event to view details, comments, and relay information.
</P> </P>
<EventSearch <EventSearch
{loading} {loading}
{error} {error}
{searchValue} {searchValue}
{dTagValue} {searchType}
{event} {event}
onEventFound={handleEventFound} onEventFound={handleEventFound}
onSearchResults={handleSearchResults} onSearchResults={handleSearchResults}
@ -513,30 +551,45 @@
{#if searchResults.length > 0} {#if searchResults.length > 0}
<div class="mt-8"> <div class="mt-8">
<div class={showSidePanel && searchResultsCollapsed ? "lg:block hidden" : "block"}> <div
class={showSidePanel && searchResultsCollapsed
? "lg:block hidden"
: "block"}
>
<Heading tag="h2" class="h-leather mb-4 break-words"> <Heading tag="h2" class="h-leather mb-4 break-words">
{#if searchType === "n"} {#if searchType === "n"}
Search Results for name: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} profiles) Search Results for name: "{searchTerm &&
searchTerm.length > 50
? searchTerm.slice(0, 50) + "..."
: searchTerm || ""}" ({searchResults.length} profiles)
{:else if searchType === "t"} {:else if searchType === "t"}
Search Results for t-tag: "{searchTerm && searchTerm.length > 50 ? searchTerm.slice(0, 50) + '...' : searchTerm || ''}" ({searchResults.length} Search Results for t-tag: "{searchTerm &&
searchTerm.length > 50
? searchTerm.slice(0, 50) + "..."
: searchTerm || ""}" ({searchResults.length}
events) events)
{:else} {:else}
Search Results for d-tag: "{(() => { Search Results for d-tag: "{(() => {
const term = searchTerm || dTagValue?.toLowerCase() || ''; const term =
return term.length > 50 ? term.slice(0, 50) + '...' : term; searchTerm ||
(searchType === "d" ? searchValue : "") ||
"";
return term.length > 50 ? term.slice(0, 50) + "..." : term;
})()}" ({searchResults.length} events) })()}" ({searchResults.length} events)
{/if} {/if}
</Heading> </Heading>
<div class="space-y-4"> <div class="space-y-4">
{#each searchResults as result, index} {#each searchResults as result, index}
{@const profileData = (result as any).profileData || parseProfileContent(result)} {@const profileData =
(result as any).profileData || parseProfileContent(result)}
<button <button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-primary-900/70 hover:bg-gray-100 dark:hover:bg-primary-800 focus:bg-gray-100 dark:focus:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden" class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-primary-900/70 hover:bg-gray-100 dark:hover:bg-primary-800 focus:bg-gray-100 dark:focus:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)} onclick={() => handleEventFound(result)}
> >
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-800 dark:text-gray-100" <span
class="font-medium text-gray-800 dark:text-gray-100"
>{searchType === "n" ? "Profile" : "Event"} >{searchType === "n" ? "Profile" : "Event"}
{index + 1}</span {index + 1}</span
> >
@ -600,26 +653,43 @@
alt="Profile" alt="Profile"
class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600" class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600"
onerror={(e) => { onerror={(e) => {
(e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).style.display =
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); "none";
(
e.target as HTMLImageElement
).nextElementSibling?.classList.remove(
"hidden",
);
}} }}
/> />
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600 hidden"> <div
<UserOutline class="w-6 h-6 text-gray-600 dark:text-gray-300" /> class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600 hidden"
>
<UserOutline
class="w-6 h-6 text-gray-600 dark:text-gray-300"
/>
</div> </div>
{:else} {:else}
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"> <div
<UserOutline class="w-6 h-6 text-gray-600 dark:text-gray-300" /> class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"
>
<UserOutline
class="w-6 h-6 text-gray-600 dark:text-gray-300"
/>
</div> </div>
{/if} {/if}
<div class="flex flex-col min-w-0 flex-1"> <div class="flex flex-col min-w-0 flex-1">
{#if profileData.display_name || profileData.name} {#if profileData.display_name || profileData.name}
<span class="font-medium text-gray-900 dark:text-gray-100 truncate"> <span
class="font-medium text-gray-900 dark:text-gray-100 truncate"
>
{profileData.display_name || profileData.name} {profileData.display_name || profileData.name}
</span> </span>
{/if} {/if}
{#if profileData.about} {#if profileData.about}
<span class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"> <span
class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"
>
{profileData.about} {profileData.about}
</span> </span>
{/if} {/if}
@ -673,7 +743,10 @@
<div <div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words" class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
> >
<EmbeddedEvent nostrIdentifier={result.id} nestingLevel={0} /> <EmbeddedEvent
nostrIdentifier={result.id}
nestingLevel={0}
/>
</div> </div>
{/if} {/if}
{/if} {/if}
@ -687,7 +760,11 @@
{#if secondOrderResults.length > 0} {#if secondOrderResults.length > 0}
<div class="mt-8"> <div class="mt-8">
<div class={showSidePanel && searchResultsCollapsed ? "lg:block hidden" : "block"}> <div
class={showSidePanel && searchResultsCollapsed
? "lg:block hidden"
: "block"}
>
<Heading tag="h2" class="h-leather mb-4"> <Heading tag="h2" class="h-leather mb-4">
Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length} Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length}
events) events)
@ -698,19 +775,21 @@
</P> </P>
{/if} {/if}
<P class="mb-4 text-sm text-gray-600 dark:text-gray-400"> <P class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Events that reference, reply to, highlight, or quote the original Events that reference, reply to, highlight, or quote the
events. original events.
</P> </P>
<div class="space-y-4"> <div class="space-y-4">
{#each secondOrderResults as result, index} {#each secondOrderResults as result, index}
{@const profileData = (result as any).profileData || parseProfileContent(result)} {@const profileData =
(result as any).profileData || parseProfileContent(result)}
<button <button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden" class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)} onclick={() => handleEventFound(result)}
> >
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-800 dark:text-gray-100" <span
class="font-medium text-gray-800 dark:text-gray-100"
>Reference {index + 1}</span >Reference {index + 1}</span
> >
<span class="text-xs text-gray-600 dark:text-gray-400" <span class="text-xs text-gray-600 dark:text-gray-400"
@ -750,7 +829,9 @@
: "Unknown date"} : "Unknown date"}
</span> </span>
</div> </div>
<div class="text-xs text-blue-600 dark:text-blue-400 mb-1"> <div
class="text-xs text-blue-600 dark:text-blue-400 mb-1"
>
{getReferenceType( {getReferenceType(
result, result,
originalEventIds, originalEventIds,
@ -765,24 +846,37 @@
alt="Profile" alt="Profile"
class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600" class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600"
onerror={(e) => { onerror={(e) => {
(e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).style.display =
"none";
}} }}
/> />
{:else} {:else}
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"> <div
<span class="text-lg font-medium text-gray-600 dark:text-gray-300"> class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"
{(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} >
<span
class="text-lg font-medium text-gray-600 dark:text-gray-300"
>
{(
profileData.display_name ||
profileData.name ||
result.pubkey.slice(0, 1)
).toUpperCase()}
</span> </span>
</div> </div>
{/if} {/if}
<div class="flex flex-col min-w-0 flex-1"> <div class="flex flex-col min-w-0 flex-1">
{#if profileData.display_name || profileData.name} {#if profileData.display_name || profileData.name}
<span class="font-medium text-gray-900 dark:text-gray-100 truncate"> <span
class="font-medium text-gray-900 dark:text-gray-100 truncate"
>
{profileData.display_name || profileData.name} {profileData.display_name || profileData.name}
</span> </span>
{/if} {/if}
{#if profileData.about} {#if profileData.about}
<span class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"> <span
class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"
>
{profileData.about} {profileData.about}
</span> </span>
{/if} {/if}
@ -836,7 +930,10 @@
<div <div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words" class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
> >
<EmbeddedEvent nostrIdentifier={result.id} nestingLevel={0} /> <EmbeddedEvent
nostrIdentifier={result.id}
nestingLevel={0}
/>
</div> </div>
{/if} {/if}
{/if} {/if}
@ -850,24 +947,31 @@
{#if tTagResults.length > 0} {#if tTagResults.length > 0}
<div class="mt-8"> <div class="mt-8">
<div class={showSidePanel && searchResultsCollapsed ? "lg:block hidden" : "block"}> <div
class={showSidePanel && searchResultsCollapsed
? "lg:block hidden"
: "block"}
>
<Heading tag="h2" class="h-leather mb-4"> <Heading tag="h2" class="h-leather mb-4">
Search Results for t-tag: "{searchTerm || Search Results for t-tag: "{searchTerm ||
dTagValue?.toLowerCase()}" ({tTagResults.length} events) (searchType === "t" ? searchValue : "")}" ({tTagResults.length}
events)
</Heading> </Heading>
<P class="mb-4 text-sm text-gray-600 dark:text-gray-400"> <P class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Events that are tagged with the t-tag. Events that are tagged with the t-tag.
</P> </P>
<div class="space-y-4"> <div class="space-y-4">
{#each tTagResults as result, index} {#each tTagResults as result, index}
{@const profileData = (result as any).profileData || parseProfileContent(result)} {@const profileData =
(result as any).profileData || parseProfileContent(result)}
<button <button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden" class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)} onclick={() => handleEventFound(result)}
> >
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-800 dark:text-gray-100" <span
class="font-medium text-gray-800 dark:text-gray-100"
>Tagged Event {index + 1}</span >Tagged Event {index + 1}</span
> >
<span class="text-xs text-gray-600 dark:text-gray-400" <span class="text-xs text-gray-600 dark:text-gray-400"
@ -915,24 +1019,37 @@
alt="Profile" alt="Profile"
class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600" class="w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-600"
onerror={(e) => { onerror={(e) => {
(e.target as HTMLImageElement).style.display = 'none'; (e.target as HTMLImageElement).style.display =
"none";
}} }}
/> />
{:else} {:else}
<div class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"> <div
<span class="text-lg font-medium text-gray-600 dark:text-gray-300"> class="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"
{(profileData.display_name || profileData.name || result.pubkey.slice(0, 1)).toUpperCase()} >
<span
class="text-lg font-medium text-gray-600 dark:text-gray-300"
>
{(
profileData.display_name ||
profileData.name ||
result.pubkey.slice(0, 1)
).toUpperCase()}
</span> </span>
</div> </div>
{/if} {/if}
<div class="flex flex-col min-w-0 flex-1"> <div class="flex flex-col min-w-0 flex-1">
{#if profileData.display_name || profileData.name} {#if profileData.display_name || profileData.name}
<span class="font-medium text-gray-900 dark:text-gray-100 truncate"> <span
class="font-medium text-gray-900 dark:text-gray-100 truncate"
>
{profileData.display_name || profileData.name} {profileData.display_name || profileData.name}
</span> </span>
{/if} {/if}
{#if profileData.about} {#if profileData.about}
<span class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"> <span
class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2"
>
{profileData.about} {profileData.about}
</span> </span>
{/if} {/if}
@ -986,7 +1103,10 @@
<div <div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words" class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
> >
<EmbeddedEvent nostrIdentifier={result.id} nestingLevel={0} /> <EmbeddedEvent
nostrIdentifier={result.id}
nestingLevel={0}
/>
</div> </div>
{/if} {/if}
{/if} {/if}
@ -998,16 +1118,29 @@
</div> </div>
{/if} {/if}
{#if !event && searchResults.length === 0 && secondOrderResults.length === 0 && tTagResults.length === 0 && !searchValue && !dTagValue && !searchInProgress} {#if !event && searchResults.length === 0 && secondOrderResults.length === 0 && tTagResults.length === 0 && !searchValue && !searchInProgress}
<div class="mt-8"> <div class="mt-8">
<Heading tag="h2" class="h-leather mb-4">Publish Nostr Event</Heading> <Heading tag="h2" class="h-leather mb-4"
>Publish Nostr Event</Heading
>
<P class="mb-4"> <P class="mb-4">
Create and publish new Nostr events to the network. This form supports various event kinds including: Create and publish new Nostr events to the network. This form
supports various event kinds including:
</P> </P>
<ul class="mb-6 list-disc list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300"> <ul
<li><strong>Kind 30040:</strong> Publication indexes that organize AsciiDoc content into structured publications</li> class="mb-6 list-disc list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300"
<li><strong>Kind 30041:</strong> Individual section content for publications</li> >
<li><strong>Other kinds:</strong> Standard Nostr events with custom tags and content</li> <li>
<strong>Kind 30040:</strong> Publication indexes that organize AsciiDoc
content into structured publications
</li>
<li>
<strong>Kind 30041:</strong> Individual section content for publications
</li>
<li>
<strong>Other kinds:</strong> Standard Nostr events with custom tags
and content
</li>
</ul> </ul>
<EventInput /> <EventInput />
</div> </div>
@ -1017,9 +1150,13 @@
<!-- Right Panel: Event Details --> <!-- Right Panel: Event Details -->
{#if showSidePanel && event} {#if showSidePanel && event}
<div class="w-full lg:flex-1 lg:min-w-0 main-leather flex flex-col space-y-6 overflow-hidden"> <div
class="w-full lg:flex-1 lg:min-w-0 main-leather flex flex-col space-y-6 overflow-hidden"
>
<div class="flex justify-between items-center min-w-0"> <div class="flex justify-between items-center min-w-0">
<Heading tag="h2" class="h-leather mb-2 break-words">Event Details</Heading> <Heading tag="h2" class="h-leather mb-2 break-words"
>Event Details</Heading
>
<button <button
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex-shrink-0" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex-shrink-0"
onclick={closeSidePanel} onclick={closeSidePanel}
@ -1062,7 +1199,9 @@
{#if user?.signedIn} {#if user?.signedIn}
<div class="mt-8 min-w-0 overflow-hidden"> <div class="mt-8 min-w-0 overflow-hidden">
<Heading tag="h3" class="h-leather mb-4 break-words">Add Comment</Heading> <Heading tag="h3" class="h-leather mb-4 break-words"
>Add Comment</Heading
>
<CommentBox {event} {userRelayPreference} /> <CommentBox {event} {userRelayPreference} />
</div> </div>
{:else} {:else}

Loading…
Cancel
Save