@ -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') change s
// Use Svelte 5 idiomatic effect to update searchValue and searchType based on URL parameter s
$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 }
{ dTagValu e}
{ searchTyp e}
{ 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 }