clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

317 lines
12 KiB

<script module lang="ts">
import type { NDKEvent } from "$lib/utils/nostrUtils";
import { NDKRelaySetFromNDK, toNpub, getUserMetadata } from "$lib/utils/nostrUtils";
import { get } from "svelte/store";
import { searchRelays } from "$lib/consts";
import { userStore, type UserState } from "$lib/stores/userStore";
import { buildCompleteRelaySet } from "$lib/utils/relay_management";
import { nip19 } from "nostr-tools";
import { parseEmbeddedMarkup } from "$lib/utils/markup/embeddedMarkupParser";
import type NDK from "@nostr-dev-kit/ndk";
export {
parsedContent,
repostContent,
quotedContent,
truncateContent,
truncateRenderedContent,
getNotificationType,
fetchAuthorProfiles
};
/**
* Truncates content to a specified length
*/
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
*/
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) + "...";
}
}
}
/**
* Gets notification type based on event kind
*/
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
*/
async function fetchAuthorProfiles(events: NDKEvent[], ndk: NDK): 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 {
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 {
if (!ndk) return;
const userStoreValue: UserState = 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] Error processing profile for ${pubkey}:`, error);
}
});
await Promise.all(profilePromises);
return authorProfiles;
}
async function findQuotedMessage(eventId: string, publicMessages: NDKEvent[], ndk: NDK): Promise<NDKEvent | undefined> {
// Validate eventId format (should be 64 character hex string)
const isValidEventId = /^[a-fA-F0-9]{64}$/.test(eventId);
if (!isValidEventId) return undefined;
// 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 {
if (ndk) {
const userStoreValue: UserState = 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(`[findQuotedMessage] Failed to fetch quoted event ${eventId}:`, error);
}
}
return quotedMessage;
}
</script>
{#snippet parsedContent(content: string)}
{#await parseEmbeddedMarkup(content, 0) then parsed}
{@html parsed}
{/await}
{/snippet}
{#snippet repostContent(content: string)}
{@const originalEvent = (() => {
try {
return JSON.parse(content);
} catch {
return null;
}
})()}
{#if originalEvent}
{@const originalContent = originalEvent.content || ""}
{@const originalAuthor = originalEvent.pubkey || ""}
{@const originalCreatedAt = originalEvent.created_at || 0}
{@const originalKind = originalEvent.kind || 1}
{@const formattedDate = originalCreatedAt ? new Date(originalCreatedAt * 1000).toLocaleDateString() : "Unknown date"}
{@const shortAuthor = originalAuthor ? `${originalAuthor.slice(0, 8)}...${originalAuthor.slice(-4)}` : "Unknown"}
<div class="embedded-repost bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 my-2">
<!-- Event header -->
<div class="flex items-center justify-between mb-3 min-w-0">
<div class="flex items-center space-x-2 min-w-0">
<span class="text-xs text-gray-500 dark:text-gray-400 font-mono flex-shrink-0">
Kind {originalKind}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
(repost)
</span>
<span class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">•</span>
<span class="text-xs text-gray-600 dark:text-gray-400 flex-shrink-0">Author:</span>
<span class="text-xs text-gray-700 dark:text-gray-300 font-mono">
{shortAuthor}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">•</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{formattedDate}
</span>
</div>
<button
class="text-xs text-primary-600 dark:text-primary-500 hover:underline flex-shrink-0"
onclick={() => window.location.href=`/events?id=${originalEvent.id || 'unknown'}`}
>
View full event →
</button>
</div>
<!-- Reposted content -->
<div class="text-sm text-gray-800 dark:text-gray-200 leading-relaxed">
{#await parseEmbeddedMarkup(originalContent, 0) then parsedOriginalContent}
{@html parsedOriginalContent}
{/await}
</div>
</div>
{:else}
{#await parseEmbeddedMarkup(content, 0) then parsedContent}
{@html parsedContent}
{/await}
{/if}
{/snippet}
{#snippet quotedContent(message: NDKEvent, publicMessages: NDKEvent[], ndk: NDK)}
{@const qTags = message.getMatchingTags("q")}
{#if qTags.length > 0}
{@const qTag = qTags[0]}
{@const eventId = qTag[1]}
{#if eventId}
{#await findQuotedMessage(eventId, publicMessages, ndk) then quotedMessage}
{#if quotedMessage}
{@const quotedContent = quotedMessage.content ? quotedMessage.content.slice(0, 200) : "No content"}
{#await parseEmbeddedMarkup(quotedContent, 0) then parsedContent}
<button type="button" class="block text-left 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 }))}>
{@html parsedContent}
</button>
{/await}
{:else}
{@const isValidEventId = /^[a-fA-F0-9]{64}$/.test(eventId)}
{#if isValidEventId}
{@const nevent = (() => {
try {
return nip19.neventEncode({ id: eventId });
} catch (error) {
console.warn(`[quotedContent] Failed to encode nevent for ${eventId}:`, error);
return null;
}
})()}
{#if nevent}
<button type="button" class="block text-left 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)}...
</button>
{:else}
<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 text-sm text-gray-600 dark:text-gray-300">
Quoted message not found. Event ID: {eventId.slice(0, 8)}...
</div>
{/if}
{:else}
<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 text-sm text-gray-600 dark:text-gray-300">
Invalid quoted message reference
</div>
{/if}
{/if}
{/await}
{/if}
{/if}
{/snippet}