Browse Source

Worked on the notifications some more

master
silberengel 7 months ago
parent
commit
1da3f6ba0d
  1. 2
      src/lib/components/EventDetails.svelte
  2. 675
      src/lib/components/Notifications.svelte

2
src/lib/components/EventDetails.svelte

@ -454,7 +454,7 @@
<div class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"> <div class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border">
<div class="flex flex-col space-y-1"> <div class="flex flex-col space-y-1">
<span class="text-gray-700 dark:text-gray-300 font-semibold">Content:</span> <span class="text-gray-700 dark:text-gray-300 font-semibold">Content:</span>
<div class="prose dark:prose-invert max-w-none"> <div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100">
{@html showFullContent ? parsedContent : contentPreview} {@html showFullContent ? parsedContent : contentPreview}
{#if !showFullContent && parsedContent.length > 250} {#if !showFullContent && parsedContent.length > 250}
<button <button

675
src/lib/components/Notifications.svelte

@ -16,154 +16,119 @@
// Component state // Component state
let notifications = $state<NDKEvent[]>([]); let notifications = $state<NDKEvent[]>([]);
let publicMessages = $state<NDKEvent[]>([]);
let loading = $state(false); let loading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let isOwnProfile = $state(false); let isOwnProfile = $state(false);
let notificationMode = $state<"to-me" | "from-me">("to-me"); let notificationMode = $state<"to-me" | "from-me" | "public-messages">("to-me");
let authorProfiles = $state<Map<string, { name?: string; displayName?: string; picture?: string }>>(new Map()); let authorProfiles = $state<Map<string, { name?: string; displayName?: string; picture?: string }>>(new Map());
let filteredByUser = $state<string | null>(null);
// Check if user is viewing their own profile
$effect(() => { // Derived state for filtered messages
if ($userStore.signedIn && $userStore.pubkey && event.pubkey) { let filteredMessages = $derived.by(() => {
isOwnProfile = $userStore.pubkey.toLowerCase() === event.pubkey.toLowerCase(); if (!filteredByUser) return publicMessages;
} else { return publicMessages.filter(message =>
isOwnProfile = false; message.pubkey === filteredByUser ||
} (message.pubkey === $userStore.pubkey && message.getMatchingTags("p").some(tag => tag[1] === filteredByUser))
);
}); });
// Fetch notifications when viewing own profile or when mode changes // AI-NOTE: Utility functions extracted to reduce code duplication
$effect(() => { function getAvailableRelays(): string[] {
if (isOwnProfile && $userStore.pubkey && $userStore.signedIn) { const userInboxRelays = $userStore.relays.inbox || [];
fetchNotifications(); const userOutboxRelays = $userStore.relays.outbox || [];
} else { const activeInboxRelayList = get(activeInboxRelays);
// Clear notifications when user logs out or is not viewing own profile
notifications = [];
authorProfiles.clear();
}
});
async function fetchNotifications() { const allRelays = [
if (!$userStore.pubkey || !isOwnProfile) return; ...userInboxRelays,
...userOutboxRelays,
...localRelays,
...communityRelays,
...activeInboxRelayList
];
loading = true; return [...new Set(allRelays)];
error = null; }
function toNpub(pubkey: string): string | null {
if (!pubkey) return null;
try { try {
const ndk = get(ndkInstance); if (/^[a-f0-9]{64}$/i.test(pubkey)) {
if (!ndk) { return nip19.npubEncode(pubkey);
throw new Error("No NDK instance available");
}
// Collect all available relays for notification discovery
const userInboxRelays = $userStore.relays.inbox || [];
const userOutboxRelays = $userStore.relays.outbox || [];
const activeInboxRelayList = get(activeInboxRelays);
// Combine user's relays, local relays, community relays, and active inbox relays
const allRelays = [
...userInboxRelays,
...userOutboxRelays,
...localRelays,
...communityRelays,
...activeInboxRelayList
];
// Remove duplicates
const uniqueRelays = [...new Set(allRelays)];
if (uniqueRelays.length === 0) {
throw new Error("No relays available for notification discovery");
}
console.log("[Notifications] Using relays for discovery:", uniqueRelays);
// Create filter based on notification mode
let filter;
if (notificationMode === "to-me") {
// Events that mention the user (notifications TO me)
filter = {
kinds: [1, 1111, 9802, 6, 16, 24],
"#p": [$userStore.pubkey], // Events that mention the user
limit: 100,
};
} else {
// Events authored by the user (notifications FROM me)
filter = {
kinds: [1, 1111, 9802, 6, 16, 24],
authors: [$userStore.pubkey], // Events authored by the user
limit: 100,
};
} }
if (pubkey.startsWith("npub1")) return pubkey;
return null;
} catch {
return null;
}
}
console.log("[Notifications] Fetching notifications for user:", $userStore.pubkey); function getNeventUrl(event: NDKEvent): string {
console.log("[Notifications] Mode:", notificationMode); const relays = getAvailableRelays();
console.log("[Notifications] Using filter:", filter); return neventEncode(event, relays);
}
// Create a relay set from all available relays
const relaySet = NDKRelaySetFromNDK.fromRelayUrls(uniqueRelays, ndk);
const events = await ndk.fetchEvents(filter, undefined, relaySet);
// Convert to array and filter out self-referential events function formatDate(timestamp: number): string {
const eventArray = Array.from(events); const date = new Date(timestamp * 1000);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
// Filter out self-referential events if (diffDays === 0) return "Today";
const filteredEvents = eventArray.filter(event => { if (diffDays === 1) return "Yesterday";
if (notificationMode === "to-me") { if (diffDays < 7) return `${diffDays} days ago`;
// For "to-me" mode, exclude events where the user is the author return date.toLocaleDateString();
return event.pubkey !== $userStore.pubkey; }
} else {
// For "from-me" mode, exclude events where the user is mentioned in p tags
const pTags = event.getMatchingTags("p");
const isUserMentioned = pTags.some(tag => tag[1] === $userStore.pubkey);
return !isUserMentioned;
}
});
// Sort by creation time (newest first) function truncateContent(content: string, maxLength: number = 300): string {
filteredEvents.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); if (content.length <= maxLength) return content;
return content.slice(0, maxLength) + "...";
}
// Take the newest 100 function getNotificationType(event: NDKEvent): string {
notifications = filteredEvents.slice(0, 100); switch (event.kind) {
case 1: return "Reply";
case 1111: return "Custom Reply";
case 9802: return "Highlight";
case 6: return "Repost";
case 16: return "Generic Repost";
case 24: return "Public Message";
default: return `Kind ${event.kind}`;
}
}
console.log("[Notifications] Found", notifications.length, "notifications"); function navigateToEvent(nevent: string) {
goto(`/events?id=${nevent}`);
}
// Fetch profiles for all notification authors function filterByUser(pubkey: string) {
await fetchAuthorProfiles(notifications); filteredByUser = filteredByUser === pubkey ? null : pubkey;
}
} catch (err) { function clearFilter() {
console.error("[Notifications] Error fetching notifications:", err); filteredByUser = null;
error = err instanceof Error ? err.message : "Failed to fetch notifications";
} finally {
loading = false;
}
} }
// Fetch author profiles with relay priority // AI-NOTE: Simplified profile fetching with better error handling
async function fetchAuthorProfiles(events: NDKEvent[]) { async function fetchAuthorProfiles(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(`[Notifications] Fetching profiles for ${uniquePubkeys.size} unique authors`);
// Fetch profiles in parallel with relay priority
const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => { const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => {
try { try {
const npub = toNpub(pubkey); const npub = toNpub(pubkey);
if (!npub) return; if (!npub) return;
// First try cache // Try cache first
let profile = await getUserMetadata(npub, false); let profile = await getUserMetadata(npub, false);
if (profile && (profile.name || profile.displayName || profile.picture)) { if (profile && (profile.name || profile.displayName || profile.picture)) {
authorProfiles.set(pubkey, profile); authorProfiles.set(pubkey, profile);
return; return;
} }
// If not in cache, try search relays first // Try search relays
const searchRelays = [ const searchRelays = [
"wss://profiles.nostr1.com", "wss://profiles.nostr1.com",
"wss://aggr.nostr.land", "wss://aggr.nostr.land",
@ -174,7 +139,6 @@
"wss://freelay.sovbit.host" "wss://freelay.sovbit.host"
]; ];
// Try search relays
for (const relay of searchRelays) { for (const relay of searchRelays) {
try { try {
const ndk = get(ndkInstance); const ndk = get(ndkInstance);
@ -189,12 +153,11 @@
if (profileEvent) { if (profileEvent) {
const profileData = JSON.parse(profileEvent.content); const profileData = JSON.parse(profileEvent.content);
const profile = { authorProfiles.set(pubkey, {
name: profileData.name, name: profileData.name,
displayName: profileData.display_name || profileData.displayName, displayName: profileData.display_name || profileData.displayName,
picture: profileData.picture || profileData.image picture: profileData.picture || profileData.image
}; });
authorProfiles.set(pubkey, profile);
return; return;
} }
} catch (error) { } catch (error) {
@ -202,26 +165,14 @@
} }
} }
// Finally try all available relays // Try all available relays as fallback
const userInboxRelays = $userStore.relays.inbox || []; const relays = getAvailableRelays();
const userOutboxRelays = $userStore.relays.outbox || []; if (relays.length > 0) {
const activeInboxRelayList = get(activeInboxRelays);
const allRelays = [
...userInboxRelays,
...userOutboxRelays,
...localRelays,
...communityRelays,
...activeInboxRelayList
];
const uniqueRelays = [...new Set(allRelays)];
if (uniqueRelays.length > 0) {
try { try {
const ndk = get(ndkInstance); const ndk = get(ndkInstance);
if (!ndk) return; if (!ndk) return;
const relaySet = NDKRelaySetFromNDK.fromRelayUrls(uniqueRelays, ndk); const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk);
const profileEvent = await ndk.fetchEvent( const profileEvent = await ndk.fetchEvent(
{ kinds: [0], authors: [pubkey] }, { kinds: [0], authors: [pubkey] },
undefined, undefined,
@ -230,102 +181,146 @@
if (profileEvent) { if (profileEvent) {
const profileData = JSON.parse(profileEvent.content); const profileData = JSON.parse(profileEvent.content);
const profile = { authorProfiles.set(pubkey, {
name: profileData.name, name: profileData.name,
displayName: profileData.display_name || profileData.displayName, displayName: profileData.display_name || profileData.displayName,
picture: profileData.picture || profileData.image picture: profileData.picture || profileData.image
}; });
authorProfiles.set(pubkey, profile);
} }
} catch (error) { } catch (error) {
console.warn(`[Notifications] Failed to fetch profile from all relays:`, error); console.warn(`[Notifications] Failed to fetch profile from all relays:`, error);
} }
} }
} catch (error) { } catch (error) {
console.warn(`[Notifications] Failed to fetch profile for ${pubkey}:`, error); console.warn(`[Notifications] Failed to fetch profile for ${pubkey}:`, error);
} }
}); });
await Promise.allSettled(profilePromises); await Promise.allSettled(profilePromises);
console.log(`[Notifications] Profile fetching complete`);
} }
function toNpub(pubkey: string): string | null { // AI-NOTE: Simplified notification fetching
if (!pubkey) return null; async function fetchNotifications() {
if (!$userStore.pubkey || !isOwnProfile) return;
loading = true;
error = null;
try { try {
if (/^[a-f0-9]{64}$/i.test(pubkey)) { const ndk = get(ndkInstance);
return nip19.npubEncode(pubkey); if (!ndk) throw new Error("No NDK instance available");
}
if (pubkey.startsWith("npub1")) return pubkey;
return null;
} catch {
return null;
}
}
function getNotificationType(event: NDKEvent): string { const relays = getAvailableRelays();
switch (event.kind) { if (relays.length === 0) throw new Error("No relays available");
case 1:
return "Reply"; const filter = {
case 1111: kinds: [1, 1111, 9802, 6, 16],
return "Custom Reply"; ...(notificationMode === "to-me"
case 9802: ? { "#p": [$userStore.pubkey] }
return "Highlight"; : { authors: [$userStore.pubkey] }
case 6: ),
return "Repost"; limit: 100,
case 16: };
return "Generic Repost";
case 24: const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk);
return "Public Message"; const events = await ndk.fetchEvents(filter, undefined, relaySet);
default: const eventArray = Array.from(events);
return `Kind ${event.kind}`;
// Filter out self-referential events
const filteredEvents = eventArray.filter(event => {
if (notificationMode === "to-me") {
return event.pubkey !== $userStore.pubkey;
} else {
const pTags = event.getMatchingTags("p");
const isUserMentioned = pTags.some(tag => tag[1] === $userStore.pubkey);
return !isUserMentioned;
}
});
notifications = filteredEvents
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
.slice(0, 100);
await fetchAuthorProfiles(notifications);
} catch (err) {
console.error("[Notifications] Error fetching notifications:", err);
error = err instanceof Error ? err.message : "Failed to fetch notifications";
} finally {
loading = false;
} }
} }
function getNeventUrl(event: NDKEvent): string { // AI-NOTE: Simplified public messages fetching - only kind 24 messages
// Use the same relay set as for fetching notifications async function fetchPublicMessages() {
const userInboxRelays = $userStore.relays.inbox || []; if (!$userStore.pubkey || !isOwnProfile) return;
const userOutboxRelays = $userStore.relays.outbox || [];
const activeInboxRelayList = get(activeInboxRelays);
const allRelays = [ loading = true;
...userInboxRelays, error = null;
...userOutboxRelays,
...localRelays,
...communityRelays,
...activeInboxRelayList
];
const uniqueRelays = [...new Set(allRelays)]; try {
return neventEncode(event, uniqueRelays); const ndk = get(ndkInstance);
} if (!ndk) throw new Error("No NDK instance available");
function navigateToEvent(nevent: string) { const relays = getAvailableRelays();
goto(`/events?id=${nevent}`); if (relays.length === 0) throw new Error("No relays available");
const relaySet = NDKRelaySetFromNDK.fromRelayUrls(relays, ndk);
// Fetch only kind 24 messages
const [messagesEvents, userMessagesEvents] = await Promise.all([
ndk.fetchEvents({ kinds: [24 as any], "#p": [$userStore.pubkey], limit: 200 }, undefined, relaySet),
ndk.fetchEvents({ kinds: [24 as any], authors: [$userStore.pubkey], limit: 200 }, undefined, relaySet)
]);
const allMessages = [
...Array.from(messagesEvents),
...Array.from(userMessagesEvents)
];
// Deduplicate and filter
const uniqueMessages = allMessages.filter((event, index, self) =>
index === self.findIndex(e => e.id === event.id)
);
publicMessages = uniqueMessages
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
.slice(0, 200);
await fetchAuthorProfiles(publicMessages);
} catch (err) {
console.error("[PublicMessages] Error fetching public messages:", err);
error = err instanceof Error ? err.message : "Failed to fetch public messages";
} finally {
loading = false;
}
} }
function formatDate(timestamp: number): string {
const date = new Date(timestamp * 1000);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return "Today"; // Check if user is viewing their own profile
} else if (diffDays === 1) { $effect(() => {
return "Yesterday"; if ($userStore.signedIn && $userStore.pubkey && event.pubkey) {
} else if (diffDays < 7) { isOwnProfile = $userStore.pubkey.toLowerCase() === event.pubkey.toLowerCase();
return `${diffDays} days ago`;
} else { } else {
return date.toLocaleDateString(); isOwnProfile = false;
} }
} });
function truncateContent(content: string, maxLength: number = 300): string { // Fetch notifications when viewing own profile or when mode changes
if (content.length <= maxLength) return content; $effect(() => {
return content.slice(0, maxLength) + "..."; if (isOwnProfile && $userStore.pubkey && $userStore.signedIn) {
} if (notificationMode === "public-messages") {
fetchPublicMessages();
} else {
fetchNotifications();
}
} else {
// Clear notifications when user logs out or is not viewing own profile
notifications = [];
publicMessages = [];
authorProfiles.clear();
}
});
</script> </script>
{#if isOwnProfile && $userStore.signedIn} {#if isOwnProfile && $userStore.signedIn}
@ -333,119 +328,229 @@
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<Heading tag="h3" class="h-leather">Notifications</Heading> <Heading tag="h3" class="h-leather">Notifications</Heading>
<!-- Toggle between "to me" and "from me" --> <!-- Mode toggle -->
<div class="flex bg-gray-200 dark:bg-gray-700 rounded-lg p-1"> <div class="flex bg-gray-300 dark:bg-gray-700 rounded-lg p-1">
<button {#each ["to-me", "from-me", "public-messages"] as mode}
class="px-3 py-1 text-sm font-medium rounded-md transition-colors {notificationMode === 'to-me' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'}" {@const modeLabel = mode === "to-me" ? "To Me" : mode === "from-me" ? "From Me" : "Public Messages"}
onclick={() => { <button
notificationMode = "to-me"; class="px-3 py-1 text-sm font-medium rounded-md transition-colors {notificationMode === mode ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-700 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'}"
}} onclick={() => notificationMode = mode as "to-me" | "from-me" | "public-messages"}
> >
To Me {modeLabel}
</button> </button>
<button {/each}
class="px-3 py-1 text-sm font-medium rounded-md transition-colors {notificationMode === 'from-me' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'}"
onclick={() => {
notificationMode = "from-me";
}}
>
From Me
</button>
</div> </div>
</div> </div>
{#if loading} {#if loading}
<div class="flex items-center justify-center py-8"> <div class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div> <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<span class="ml-2 text-gray-600 dark:text-gray-400">Loading notifications...</span> <span class="ml-2 text-gray-600 dark:text-gray-400">
Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}...
</span>
</div> </div>
{:else if error} {:else if error}
<div class="p-4 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded-lg"> <div class="p-4 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 rounded-lg">
<P>Error loading notifications: {error}</P> <P>Error loading {notificationMode === "public-messages" ? "public messages" : "notifications"}: {error}</P>
</div>
{:else if notifications.length === 0}
<div class="p-4 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-lg">
<P>No notifications {notificationMode === "to-me" ? "received" : "sent"} found.</P>
</div> </div>
{:else} {:else if notificationMode === "public-messages"}
<div class="max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg"> {#if publicMessages.length === 0}
<div class="divide-y divide-gray-200 dark:divide-gray-700"> <div class="p-4 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-lg">
{#each notifications.slice(0, 10) as notification} <P>No public messages found.</P>
{@const authorProfile = authorProfiles.get(notification.pubkey)} </div>
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"> {:else}
<div class="flex items-start gap-3"> <div class="max-h-[72rem] overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg">
<!-- Author Profile Picture --> {#if filteredByUser}
<div class="flex-shrink-0"> <div class="p-3 bg-blue-50 dark:bg-blue-900 border-b border-gray-200 dark:border-gray-700">
{#if authorProfile?.picture} <div class="flex items-center justify-between">
<img <span class="text-sm text-blue-700 dark:text-blue-300">
src={authorProfile.picture} Filtered by user: {authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name || `${filteredByUser.slice(0, 8)}...${filteredByUser.slice(-4)}`}
alt="Author avatar" </span>
class="w-10 h-10 rounded-full object-cover border border-gray-200 dark:border-gray-600" <button
onerror={(e) => { class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 underline font-medium"
(e.target as HTMLImageElement).style.display = 'none'; onclick={clearFilter}
}} >
/> Clear Filter
{:else} </button>
<div class="w-10 h-10 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-sm font-medium text-gray-600 dark:text-gray-300"> </div>
{(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} {/if}
<div class="divide-y divide-gray-200 dark:divide-gray-700">
{#each filteredMessages.slice(0, 20) as message}
{@const authorProfile = authorProfiles.get(message.pubkey)}
{@const isFromUser = message.pubkey === $userStore.pubkey}
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-start gap-3 {isFromUser ? 'flex-row-reverse' : ''}">
<!-- Author Profile Picture -->
<div class="flex-shrink-0 relative">
{#if authorProfile?.picture}
<img
src={authorProfile.picture}
alt="Author avatar"
class="w-10 h-10 rounded-full object-cover border border-gray-200 dark:border-gray-600"
onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'}
/>
{:else}
<div class="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600">
<span class="text-sm font-medium text-gray-600 dark:text-gray-300">
{(authorProfile?.displayName || authorProfile?.name || message.pubkey.slice(0, 1)).toUpperCase()}
</span>
</div>
{/if}
<!-- Filter button for non-user messages -->
{#if !isFromUser}
<div class="mt-2 flex justify-center">
<button
class="w-6 h-6 bg-gray-400 hover:bg-gray-500 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-300 rounded-full flex items-center justify-center text-xs shadow-sm transition-colors {filteredByUser === message.pubkey ? 'ring-2 ring-gray-300 dark:ring-gray-400 bg-gray-500 dark:bg-gray-500' : ''}"
onclick={() => filterByUser(message.pubkey)}
title="Filter by this user"
aria-label="Filter by this user"
>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z" clip-rule="evenodd" />
</svg>
</button>
</div>
{/if}
</div>
<!-- Message Content -->
<div class="flex-1 min-w-0 {isFromUser ? 'text-right' : ''}">
<div class="flex items-center gap-2 mb-2 {isFromUser ? 'justify-end' : ''}">
<span class="text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-900 px-2 py-1 rounded">
{isFromUser ? 'Your Message' : 'Public Message'}
</span> </span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{message.created_at ? formatDate(message.created_at) : "Unknown date"}
</span>
</div>
<!-- Author Name -->
<div class="mb-2 {isFromUser ? 'text-right' : ''}">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
{authorProfile?.displayName || authorProfile?.name || `${message.pubkey.slice(0, 8)}...${message.pubkey.slice(-4)}`}
</span>
{#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName}
<span class="text-xs text-gray-500 dark:text-gray-400 ml-1">
(@{authorProfile.name})
</span>
{/if}
</div> </div>
{/if}
{#if message.content}
<div class="text-sm text-gray-800 dark:text-gray-200 mb-2 leading-relaxed">
{truncateContent(message.content)}
</div>
{/if}
<div class="flex items-center gap-2 {isFromUser ? 'justify-end' : ''}">
<button
class="text-xs text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-200 underline font-medium"
onclick={() => navigateToEvent(getNeventUrl(message))}
>
View Event
</button>
<span class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{getNeventUrl(message).slice(0, 16)}...
</span>
</div>
</div>
</div> </div>
</div>
{/each}
</div>
<!-- Notification Content --> {#if filteredMessages.length > 20}
<div class="flex-1 min-w-0"> <div class="p-4 text-center text-sm text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-2 mb-2"> Showing 20 of {filteredMessages.length} messages {filteredByUser ? `(filtered)` : ''}. Scroll to see more.
<span class="text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-900 px-2 py-1 rounded"> </div>
{getNotificationType(notification)} {/if}
</span> </div>
<span class="text-xs text-gray-500 dark:text-gray-400"> {/if}
{notification.created_at ? formatDate(notification.created_at) : "Unknown date"} {:else}
</span> {#if notifications.length === 0}
<div class="p-4 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-lg">
<P>No notifications {notificationMode === "to-me" ? "received" : "sent"} found.</P>
</div>
{:else}
<div class="max-h-[72rem] overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-lg">
<div class="divide-y divide-gray-200 dark:divide-gray-700">
{#each notifications.slice(0, 10) as notification}
{@const authorProfile = authorProfiles.get(notification.pubkey)}
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<div class="flex items-start gap-3">
<!-- Author Profile Picture -->
<div class="flex-shrink-0">
{#if authorProfile?.picture}
<img
src={authorProfile.picture}
alt="Author avatar"
class="w-10 h-10 rounded-full object-cover border border-gray-200 dark:border-gray-600"
onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'}
/>
{:else}
<div class="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600">
<span class="text-sm font-medium text-gray-600 dark:text-gray-300">
{(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()}
</span>
</div>
{/if}
</div> </div>
<!-- Author Name --> <!-- Notification Content -->
<div class="mb-2"> <div class="flex-1 min-w-0">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100"> <div class="flex items-center gap-2 mb-2">
{authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`} <span class="text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-900 px-2 py-1 rounded">
</span> {getNotificationType(notification)}
{#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName} </span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-1"> <span class="text-xs text-gray-500 dark:text-gray-400">
(@{authorProfile.name}) {notification.created_at ? formatDate(notification.created_at) : "Unknown date"}
</span>
</div>
<!-- Author Name -->
<div class="mb-2">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
{authorProfile?.displayName || authorProfile?.name || `${notification.pubkey.slice(0, 8)}...${notification.pubkey.slice(-4)}`}
</span> </span>
{#if authorProfile?.name && authorProfile?.displayName && authorProfile.name !== authorProfile.displayName}
<span class="text-xs text-gray-500 dark:text-gray-400 ml-1">
(@{authorProfile.name})
</span>
{/if}
</div>
{#if notification.content}
<div class="text-sm text-gray-800 dark:text-gray-200 mb-2 leading-relaxed">
{truncateContent(notification.content)}
</div>
{/if} {/if}
</div>
{#if notification.content} <div class="flex items-center gap-2">
<div class="text-sm text-gray-800 dark:text-gray-200 mb-2 leading-relaxed"> <button
{truncateContent(notification.content)} class="text-xs text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-200 underline font-medium"
onclick={() => navigateToEvent(getNeventUrl(notification))}
>
View Event
</button>
<span class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{getNeventUrl(notification).slice(0, 16)}...
</span>
</div> </div>
{/if}
<div class="flex items-center gap-2">
<button
class="text-xs text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-200 underline font-medium"
onclick={() => navigateToEvent(getNeventUrl(notification))}
>
View Event
</button>
<span class="text-xs text-gray-500 dark:text-gray-400 font-mono">
{getNeventUrl(notification).slice(0, 16)}...
</span>
</div> </div>
</div> </div>
</div> </div>
{/each}
</div>
{#if notifications.length > 10}
<div class="p-4 text-center text-sm text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
</div> </div>
{/each} {/if}
</div> </div>
{/if}
{#if notifications.length > 10}
<div class="p-4 text-center text-sm text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
Showing 10 of {notifications.length} notifications {notificationMode === "to-me" ? "received" : "sent"}. Scroll to see more.
</div>
{/if}
</div>
{/if} {/if}
</div> </div>
{/if} {/if}
Loading…
Cancel
Save