3 changed files with 386 additions and 476 deletions
@ -0,0 +1,225 @@ |
|||||||
|
import { parseBasicmarkup } from "$lib/utils/markup/basicMarkupParser"; |
||||||
|
import type { NDKEvent } from "$lib/utils/nostrUtils"; |
||||||
|
import { getUserMetadata, NDKRelaySetFromNDK, toNpub } from "$lib/utils/nostrUtils"; |
||||||
|
import { get } from "svelte/store"; |
||||||
|
import { ndkInstance } from "$lib/ndk"; |
||||||
|
import { searchRelays } from "$lib/consts"; |
||||||
|
import { userStore } from "$lib/stores/userStore"; |
||||||
|
import { buildCompleteRelaySet } from "$lib/utils/relay_management"; |
||||||
|
import { neventEncode } from "$lib/utils"; |
||||||
|
|
||||||
|
// AI-NOTE: Notification-specific utility functions that don't exist elsewhere
|
||||||
|
|
||||||
|
/** |
||||||
|
* Truncates content to a specified length |
||||||
|
*/ |
||||||
|
export function truncateContent(content: string, maxLength: number = 300): string { |
||||||
|
if (content.length <= maxLength) return content; |
||||||
|
return content.slice(0, maxLength) + "..."; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Truncates rendered HTML content while preserving quote boxes |
||||||
|
*/ |
||||||
|
export function truncateRenderedContent(renderedHtml: string, maxLength: number = 300): string { |
||||||
|
if (renderedHtml.length <= maxLength) return renderedHtml; |
||||||
|
|
||||||
|
const hasQuoteBoxes = renderedHtml.includes('jump-to-message'); |
||||||
|
|
||||||
|
if (hasQuoteBoxes) { |
||||||
|
const quoteBoxPattern = /<div class="block w-fit my-2 px-3 py-2 bg-gray-200[^>]*onclick="window\.dispatchEvent\(new CustomEvent\('jump-to-message'[^>]*>[^<]*<\/div>/g; |
||||||
|
const quoteBoxes = renderedHtml.match(quoteBoxPattern) || []; |
||||||
|
|
||||||
|
let textOnly = renderedHtml.replace(quoteBoxPattern, '|||QUOTEBOX|||'); |
||||||
|
|
||||||
|
if (textOnly.length > maxLength) { |
||||||
|
const availableLength = maxLength - (quoteBoxes.join('').length); |
||||||
|
if (availableLength > 50) { |
||||||
|
textOnly = textOnly.slice(0, availableLength) + "..."; |
||||||
|
} else { |
||||||
|
textOnly = textOnly.slice(0, 50) + "..."; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let result = textOnly; |
||||||
|
quoteBoxes.forEach(box => { |
||||||
|
result = result.replace('|||QUOTEBOX|||', box); |
||||||
|
}); |
||||||
|
|
||||||
|
return result; |
||||||
|
} else { |
||||||
|
if (renderedHtml.includes('<')) { |
||||||
|
const truncated = renderedHtml.slice(0, maxLength); |
||||||
|
const lastTagStart = truncated.lastIndexOf('<'); |
||||||
|
const lastTagEnd = truncated.lastIndexOf('>'); |
||||||
|
|
||||||
|
if (lastTagStart > lastTagEnd) { |
||||||
|
return renderedHtml.slice(0, lastTagStart) + "..."; |
||||||
|
} |
||||||
|
return truncated + "..."; |
||||||
|
} else { |
||||||
|
return renderedHtml.slice(0, maxLength) + "..."; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parses content using basic markup parser |
||||||
|
*/ |
||||||
|
export async function parseContent(content: string): Promise<string> { |
||||||
|
if (!content) return ""; |
||||||
|
return await parseBasicmarkup(content); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Renders quoted content for a message |
||||||
|
*/ |
||||||
|
export async function renderQuotedContent(message: NDKEvent, publicMessages: NDKEvent[]): Promise<string> { |
||||||
|
const qTags = message.getMatchingTags("q"); |
||||||
|
if (qTags.length === 0) return ""; |
||||||
|
|
||||||
|
const qTag = qTags[0]; |
||||||
|
const eventId = qTag[1]; |
||||||
|
|
||||||
|
if (eventId) { |
||||||
|
// First try to find in local messages
|
||||||
|
let quotedMessage = publicMessages.find(msg => msg.id === eventId); |
||||||
|
|
||||||
|
// If not found locally, fetch from relays
|
||||||
|
if (!quotedMessage) { |
||||||
|
try { |
||||||
|
const ndk = get(ndkInstance); |
||||||
|
if (ndk) { |
||||||
|
const userStoreValue = get(userStore); |
||||||
|
const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; |
||||||
|
const relaySet = await buildCompleteRelaySet(ndk, user); |
||||||
|
const allRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays, ...searchRelays]; |
||||||
|
|
||||||
|
if (allRelays.length > 0) { |
||||||
|
const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); |
||||||
|
const fetchedEvent = await ndk.fetchEvent({ ids: [eventId], limit: 1 }, undefined, ndkRelaySet); |
||||||
|
quotedMessage = fetchedEvent || undefined; |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.warn(`[renderQuotedContent] Failed to fetch quoted event ${eventId}:`, error); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (quotedMessage) { |
||||||
|
const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"; |
||||||
|
const parsedContent = await parseBasicmarkup(quotedContent); |
||||||
|
return `<div class="block w-fit my-2 px-3 py-2 bg-gray-200 dark:bg-gray-700 border-l-2 border-gray-400 dark:border-gray-500 rounded cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors text-sm text-gray-600 dark:text-gray-300" onclick="window.dispatchEvent(new CustomEvent('jump-to-message', { detail: '${eventId}' }))">${parsedContent}</div>`; |
||||||
|
} else { |
||||||
|
// Fallback to nevent link
|
||||||
|
const nevent = neventEncode({ id: eventId } as any, []); |
||||||
|
return `<div class="block w-fit my-2 px-3 py-2 bg-gray-200 dark:bg-gray-700 border-l-2 border-gray-400 dark:border-gray-500 rounded cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors text-sm text-gray-600 dark:text-gray-300" onclick="window.location.href='/events?id=${nevent}'">Quoted message not found. Click to view event ${eventId.slice(0, 8)}...</div>`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return "";
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets notification type based on event kind |
||||||
|
*/ |
||||||
|
export function getNotificationType(event: NDKEvent): string { |
||||||
|
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}`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches author profiles for a list of events |
||||||
|
*/ |
||||||
|
export async function fetchAuthorProfiles(events: NDKEvent[]): Promise<Map<string, { name?: string; displayName?: string; picture?: string }>> { |
||||||
|
const authorProfiles = new Map<string, { name?: string; displayName?: string; picture?: string }>(); |
||||||
|
const uniquePubkeys = new Set<string>(); |
||||||
|
|
||||||
|
events.forEach(event => { |
||||||
|
if (event.pubkey) uniquePubkeys.add(event.pubkey); |
||||||
|
}); |
||||||
|
|
||||||
|
const profilePromises = Array.from(uniquePubkeys).map(async (pubkey) => { |
||||||
|
try { |
||||||
|
const npub = toNpub(pubkey); |
||||||
|
if (!npub) return; |
||||||
|
|
||||||
|
// Try cache first
|
||||||
|
let profile = await getUserMetadata(npub, false); |
||||||
|
if (profile && (profile.name || profile.displayName || profile.picture)) { |
||||||
|
authorProfiles.set(pubkey, profile); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Try search relays
|
||||||
|
for (const relay of searchRelays) { |
||||||
|
try { |
||||||
|
const ndk = get(ndkInstance); |
||||||
|
if (!ndk) break; |
||||||
|
|
||||||
|
const relaySet = NDKRelaySetFromNDK.fromRelayUrls([relay], ndk); |
||||||
|
const profileEvent = await ndk.fetchEvent( |
||||||
|
{ kinds: [0], authors: [pubkey] }, |
||||||
|
undefined, |
||||||
|
relaySet |
||||||
|
); |
||||||
|
|
||||||
|
if (profileEvent) { |
||||||
|
const profileData = JSON.parse(profileEvent.content); |
||||||
|
authorProfiles.set(pubkey, { |
||||||
|
name: profileData.name, |
||||||
|
displayName: profileData.display_name || profileData.displayName, |
||||||
|
picture: profileData.picture || profileData.image |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.warn(`[fetchAuthorProfiles] Failed to fetch profile from ${relay}:`, error); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Try all available relays as fallback
|
||||||
|
try { |
||||||
|
const ndk = get(ndkInstance); |
||||||
|
if (!ndk) return; |
||||||
|
|
||||||
|
const userStoreValue = get(userStore); |
||||||
|
const user = userStoreValue.signedIn && userStoreValue.pubkey ? ndk.getUser({ pubkey: userStoreValue.pubkey }) : null; |
||||||
|
const relaySet = await buildCompleteRelaySet(ndk, user); |
||||||
|
const allRelays = [...relaySet.inboxRelays, ...relaySet.outboxRelays]; |
||||||
|
|
||||||
|
if (allRelays.length > 0) { |
||||||
|
const ndkRelaySet = NDKRelaySetFromNDK.fromRelayUrls(allRelays, ndk); |
||||||
|
const profileEvent = await ndk.fetchEvent( |
||||||
|
{ kinds: [0], authors: [pubkey] }, |
||||||
|
undefined, |
||||||
|
ndkRelaySet |
||||||
|
); |
||||||
|
|
||||||
|
if (profileEvent) { |
||||||
|
const profileData = JSON.parse(profileEvent.content); |
||||||
|
authorProfiles.set(pubkey, { |
||||||
|
name: profileData.name, |
||||||
|
displayName: profileData.display_name || profileData.displayName, |
||||||
|
picture: profileData.picture || profileData.image |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.warn(`[fetchAuthorProfiles] Failed to fetch profile from all relays:`, error); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.warn(`[fetchAuthorProfiles] Failed to fetch profile for ${pubkey}:`, error); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
await Promise.allSettled(profilePromises); |
||||||
|
return authorProfiles; |
||||||
|
} |
||||||
Loading…
Reference in new issue