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.
 
 
 
 
 

272 lines
7.7 KiB

<script lang="ts">
import type { NostrEvent } from '../../types/nostr.js';
import EventMenu from '../EventMenu.svelte';
import { getEventLink } from '../../services/event-links.js';
import { goto } from '$app/navigation';
import IconButton from '../ui/IconButton.svelte';
import { sessionManager } from '../../services/auth/session-manager.js';
import MediaAttachments from './MediaAttachments.svelte';
import { KIND } from '../../types/kind-lookup.js';
import { page } from '$app/stores';
interface Props {
event: NostrEvent;
showMenu?: boolean;
hideTitle?: boolean; // If true, don't show title (already displayed elsewhere)
hideImageIfInMedia?: boolean; // If true, check if image is already in MediaAttachments
}
let { event, showMenu = true, hideTitle = false, hideImageIfInMedia = true }: Props = $props();
// Normalize URL for comparison (same logic as MediaAttachments)
function normalizeUrl(url: string): string {
try {
const parsed = new URL(url);
// Remove query params and fragments for comparison
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`.replace(/\/$/, '');
} catch {
return url;
}
}
// Check if image URL is already in imeta tags
function isImageInImeta(imageUrl: string): boolean {
if (!imageUrl) return false;
const normalizedImageUrl = normalizeUrl(imageUrl);
for (const tag of event.tags) {
if (tag[0] === 'imeta') {
for (let i = 1; i < tag.length; i++) {
const item = tag[i];
if (item.startsWith('url ')) {
const imetaUrl = item.substring(4).trim();
if (normalizeUrl(imetaUrl) === normalizedImageUrl) {
return true;
}
}
}
}
}
return false;
}
// Extract metadata tags (using $derived for reactivity)
const rawImage = $derived(event.tags.find(t => t[0] === 'image' && t[1])?.[1]);
const image = $derived(rawImage && (!hideImageIfInMedia || !isImageInImeta(rawImage)) ? rawImage : null);
const description = $derived(event.tags.find(t => t[0] === 'description' && t[1])?.[1]);
const summary = $derived(event.tags.find(t => t[0] === 'summary' && t[1])?.[1]);
const author = $derived(event.tags.find(t => t[0] === 'author' && t[1])?.[1]);
const title = $derived(
hideTitle ? null :
(event.tags.find(t => t[0] === 'title' && t[1])?.[1] ||
(() => {
// Fallback to d-tag in Title Case
const dTag = event.tags.find(t => t[0] === 'd' && t[1])?.[1];
if (dTag) {
return dTag.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join(' ');
}
return null;
})())
);
const hasMetadata = $derived(image || description || summary || author || title);
const hasContent = $derived(event.content && event.content.trim().length > 0);
const shouldShowMetadata = $derived(hasMetadata || !hasContent); // Show metadata if it exists OR if there's no content
// Media kinds that should auto-render media (except on /feed)
const MEDIA_KINDS: number[] = [KIND.PICTURE_NOTE, KIND.VIDEO_NOTE, KIND.SHORT_VIDEO_NOTE, KIND.VOICE_NOTE, KIND.VOICE_REPLY];
const isMediaKind = $derived(MEDIA_KINDS.includes(event.kind));
const isOnFeedPage = $derived($page.url.pathname === '/feed');
const shouldAutoRenderMedia = $derived(isMediaKind && !isOnFeedPage);
</script>
{#if shouldShowMetadata}
<div class="metadata-card">
<div class="metadata-header">
{#if title}
<h2 class="metadata-title">{title}</h2>
{/if}
{#if showMenu}
<div class="flex items-center gap-2">
<IconButton
icon="eye"
label="View"
size={16}
onclick={() => goto(getEventLink(event))}
/>
{#if sessionManager.isLoggedIn()}
<IconButton
icon="message-square"
label="Reply"
size={16}
onclick={() => {}}
/>
{/if}
<EventMenu event={event} showContentActions={false} />
</div>
{/if}
</div>
{#if image}
<div class="metadata-image">
<img
src={image}
alt={title || description || summary || 'Metadata image'}
/>
</div>
{/if}
{#if shouldAutoRenderMedia}
<MediaAttachments event={event} forceRender={isMediaKind} />
{/if}
<div class="metadata-content">
{#if description}
<p class="metadata-description">{description}</p>
{/if}
{#if summary}
<p class="metadata-summary">{summary}</p>
{/if}
{#if author}
<p class="metadata-author">Author: {author}</p>
{/if}
{#if !hasContent && !isMediaKind}
<!-- Show all tags when there's no content, but skip for media kinds (MediaAttachments handles those) -->
<div class="metadata-tags">
{#each event.tags as tag}
{#if tag[0] !== 'image' && tag[0] !== 'description' && tag[0] !== 'summary' && tag[0] !== 'author' && tag[0] !== 'title' && tag[0] !== 'd' && tag[0] !== 'imeta' && tag[0] !== 'file' && tag[0] !== 'alt' && tag[0] !== 'x' && tag[0] !== 'm'}
<div class="metadata-tag">
<span class="metadata-tag-name">{tag[0]}:</span>
{#each tag.slice(1) as value}
{#if value}
<span class="metadata-tag-value">{value}</span>
{/if}
{/each}
</div>
{/if}
{/each}
</div>
{/if}
</div>
</div>
{/if}
<style>
.metadata-card {
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
background: var(--fog-post, #ffffff);
}
:global(.dark) .metadata-card {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
}
.metadata-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.metadata-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
flex: 1;
}
:global(.dark) .metadata-title {
color: var(--fog-dark-text, #f9fafb);
}
.metadata-image {
margin-bottom: 1rem;
border-radius: 0.5rem;
overflow: hidden;
}
.metadata-image img {
max-width: 600px;
width: 100%;
height: auto;
display: block;
}
.metadata-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.metadata-description,
.metadata-summary {
margin: 0;
color: var(--fog-text, #1f2937);
line-height: 1.6;
}
:global(.dark) .metadata-description,
:global(.dark) .metadata-summary {
color: var(--fog-dark-text, #f9fafb);
}
.metadata-author {
margin: 0;
font-size: 0.875rem;
color: var(--fog-text-light, #52667a);
}
:global(.dark) .metadata-author {
color: var(--fog-dark-text-light, #a8b8d0);
}
.metadata-tags {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
:global(.dark) .metadata-tags {
border-top-color: var(--fog-dark-border, #374151);
}
.metadata-tag {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: baseline;
font-size: 0.875rem;
}
.metadata-tag-name {
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .metadata-tag-name {
color: var(--fog-dark-text, #f9fafb);
}
.metadata-tag-value {
color: var(--fog-text-light, #52667a);
font-family: monospace;
word-break: break-all;
}
:global(.dark) .metadata-tag-value {
color: var(--fog-dark-text-light, #a8b8d0);
}
</style>