|
|
|
@ -93,6 +93,8 @@ |
|
|
|
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null); |
|
|
|
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null); |
|
|
|
let showJsonModal = $state(false); |
|
|
|
let showJsonModal = $state(false); |
|
|
|
let showPreviewModal = $state(false); |
|
|
|
let showPreviewModal = $state(false); |
|
|
|
|
|
|
|
let previewContent = $state<string>(''); |
|
|
|
|
|
|
|
let previewEvent = $state<NostrEvent | null>(null); |
|
|
|
let showExampleModal = $state(false); |
|
|
|
let showExampleModal = $state(false); |
|
|
|
let showAdvancedEditor = $state(false); |
|
|
|
let showAdvancedEditor = $state(false); |
|
|
|
let richTextEditorRef: { clearUploadedFiles: () => void; getUploadedFiles: () => Array<{ url: string; imetaTag: string[] }> } | null = $state(null); |
|
|
|
let richTextEditorRef: { clearUploadedFiles: () => void; getUploadedFiles: () => Array<{ url: string; imetaTag: string[] }> } | null = $state(null); |
|
|
|
@ -228,12 +230,16 @@ |
|
|
|
allTags.push(['client', 'aitherboard']); |
|
|
|
allTags.push(['client', 'aitherboard']); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process content to add "nostr:" prefix to valid Nostr addresses |
|
|
|
|
|
|
|
const { processNostrLinks } = await import('../../utils/nostr-link-processor.js'); |
|
|
|
|
|
|
|
const processedContent = processNostrLinks(contentWithUrls.trim()); |
|
|
|
|
|
|
|
|
|
|
|
const event: Omit<NostrEvent, 'id' | 'sig'> = { |
|
|
|
const event: Omit<NostrEvent, 'id' | 'sig'> = { |
|
|
|
kind: effectiveKind, |
|
|
|
kind: effectiveKind, |
|
|
|
pubkey: session.pubkey, |
|
|
|
pubkey: session.pubkey, |
|
|
|
created_at: Math.floor(Date.now() / 1000), |
|
|
|
created_at: Math.floor(Date.now() / 1000), |
|
|
|
tags: allTags, |
|
|
|
tags: allTags, |
|
|
|
content: contentWithUrls.trim() |
|
|
|
content: processedContent |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
return JSON.stringify(event, null, 2); |
|
|
|
return JSON.stringify(event, null, 2); |
|
|
|
@ -527,7 +533,66 @@ |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
type="button" |
|
|
|
onclick={() => showPreviewModal = true} |
|
|
|
onclick={async () => { |
|
|
|
|
|
|
|
// Generate preview content with all processing applied |
|
|
|
|
|
|
|
let contentWithUrls = content.trim(); |
|
|
|
|
|
|
|
for (const file of uploadedFiles) { |
|
|
|
|
|
|
|
if (!contentWithUrls.includes(file.url)) { |
|
|
|
|
|
|
|
if (contentWithUrls && !contentWithUrls.endsWith('\n')) { |
|
|
|
|
|
|
|
contentWithUrls += '\n'; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
contentWithUrls += `${file.url}\n`; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process content to add "nostr:" prefix |
|
|
|
|
|
|
|
const { processNostrLinks } = await import('../../utils/nostr-link-processor.js'); |
|
|
|
|
|
|
|
previewContent = processNostrLinks(contentWithUrls.trim()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Build preview event with all tags |
|
|
|
|
|
|
|
const previewTags: string[][] = []; |
|
|
|
|
|
|
|
for (const tag of tags) { |
|
|
|
|
|
|
|
if (tag[0] && tag[1]) { |
|
|
|
|
|
|
|
previewTags.push([...tag]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for (const file of uploadedFiles) { |
|
|
|
|
|
|
|
previewTags.push(file.imetaTag); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-extract tags |
|
|
|
|
|
|
|
const autoTagsResult = await autoExtractTags({ |
|
|
|
|
|
|
|
content: contentWithUrls, |
|
|
|
|
|
|
|
existingTags: previewTags, |
|
|
|
|
|
|
|
kind: effectiveKind |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
previewTags.push(...autoTagsResult.tags); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// For parameterized replaceable events, ensure d-tag exists |
|
|
|
|
|
|
|
if (isParameterizedReplaceableKind(effectiveKind)) { |
|
|
|
|
|
|
|
const dTagResult = ensureDTagForParameterizedReplaceable(previewTags, effectiveKind); |
|
|
|
|
|
|
|
if (dTagResult) { |
|
|
|
|
|
|
|
previewTags.push(['d', dTagResult.dTag]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Include client tag if selected |
|
|
|
|
|
|
|
if (shouldIncludeClientTag()) { |
|
|
|
|
|
|
|
previewTags.push(['client', 'aitherboard']); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
previewEvent = { |
|
|
|
|
|
|
|
kind: effectiveKind, |
|
|
|
|
|
|
|
pubkey: sessionManager.getCurrentPubkey() || '', |
|
|
|
|
|
|
|
created_at: Math.floor(Date.now() / 1000), |
|
|
|
|
|
|
|
tags: previewTags, |
|
|
|
|
|
|
|
content: previewContent, |
|
|
|
|
|
|
|
id: '', |
|
|
|
|
|
|
|
sig: '' |
|
|
|
|
|
|
|
} as NostrEvent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showPreviewModal = true; |
|
|
|
|
|
|
|
}} |
|
|
|
class="content-button" |
|
|
|
class="content-button" |
|
|
|
disabled={publishing} |
|
|
|
disabled={publishing} |
|
|
|
title="Preview" |
|
|
|
title="Preview" |
|
|
|
@ -674,56 +739,49 @@ |
|
|
|
<button onclick={() => showPreviewModal = false} class="close-button">×</button> |
|
|
|
<button onclick={() => showPreviewModal = false} class="close-button">×</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="modal-body preview-body"> |
|
|
|
<div class="modal-body preview-body"> |
|
|
|
{#if content.trim() || uploadedFiles.length > 0} |
|
|
|
{#if previewEvent && previewContent} |
|
|
|
{@const previewContent = (() => { |
|
|
|
<!-- Essential Metadata Display --> |
|
|
|
let contentWithUrls = content.trim(); |
|
|
|
{@const titleTag = previewEvent.tags.find(t => (t[0] === 'title' || t[0] === 'T') && t[1])} |
|
|
|
for (const file of uploadedFiles) { |
|
|
|
{@const authorTag = previewEvent.tags.find(t => t[0] === 'author' && t[1])} |
|
|
|
// Add URL to content field only if it's not already there |
|
|
|
{@const summaryTag = previewEvent.tags.find(t => t[0] === 'summary' && t[1])} |
|
|
|
// (to avoid duplicates if URL was already inserted into textarea) |
|
|
|
{@const descriptionTag = previewEvent.tags.find(t => t[0] === 'description' && t[1])} |
|
|
|
if (!contentWithUrls.includes(file.url)) { |
|
|
|
{@const imageTag = previewEvent.tags.find(t => t[0] === 'image' && t[1])} |
|
|
|
if (contentWithUrls && !contentWithUrls.endsWith('\n')) { |
|
|
|
|
|
|
|
contentWithUrls += '\n'; |
|
|
|
{#if titleTag || authorTag || summaryTag || descriptionTag || imageTag} |
|
|
|
} |
|
|
|
<div class="preview-metadata"> |
|
|
|
contentWithUrls += `${file.url}\n`; |
|
|
|
{#if titleTag} |
|
|
|
} |
|
|
|
<div class="metadata-item"> |
|
|
|
} |
|
|
|
<strong class="metadata-label">Title:</strong> |
|
|
|
return contentWithUrls.trim(); |
|
|
|
<span class="metadata-value">{titleTag[1]}</span> |
|
|
|
})()} |
|
|
|
</div> |
|
|
|
{@const previewEvent = (() => { |
|
|
|
{/if} |
|
|
|
// Create a mock event for MediaAttachments to process |
|
|
|
{#if authorTag} |
|
|
|
// MediaAttachments will skip imeta tags if URL is already in content |
|
|
|
<div class="metadata-item"> |
|
|
|
const previewTags: string[][] = []; |
|
|
|
<strong class="metadata-label">Author:</strong> |
|
|
|
|
|
|
|
<span class="metadata-value">{authorTag[1]}</span> |
|
|
|
// Include existing tags (like image tags, etc.) |
|
|
|
</div> |
|
|
|
for (const tag of tags) { |
|
|
|
{/if} |
|
|
|
if (tag[0] && tag[1]) { |
|
|
|
{#if summaryTag} |
|
|
|
previewTags.push([...tag]); |
|
|
|
<div class="metadata-item"> |
|
|
|
} |
|
|
|
<strong class="metadata-label">Summary:</strong> |
|
|
|
} |
|
|
|
<span class="metadata-value">{summaryTag[1]}</span> |
|
|
|
|
|
|
|
</div> |
|
|
|
// Add imeta tags from uploaded files |
|
|
|
{/if} |
|
|
|
for (const file of uploadedFiles) { |
|
|
|
{#if descriptionTag} |
|
|
|
previewTags.push(file.imetaTag); |
|
|
|
<div class="metadata-item"> |
|
|
|
} |
|
|
|
<strong class="metadata-label">Description:</strong> |
|
|
|
|
|
|
|
<span class="metadata-value">{descriptionTag[1]}</span> |
|
|
|
// For parameterized replaceable events, ensure d-tag exists |
|
|
|
</div> |
|
|
|
if (isParameterizedReplaceableKind(effectiveKind)) { |
|
|
|
{/if} |
|
|
|
const dTagResult = ensureDTagForParameterizedReplaceable(previewTags, effectiveKind); |
|
|
|
{#if imageTag} |
|
|
|
if (dTagResult) { |
|
|
|
<div class="metadata-item"> |
|
|
|
previewTags.push(['d', dTagResult.dTag]); |
|
|
|
<strong class="metadata-label">Image:</strong> |
|
|
|
} |
|
|
|
<img src={imageTag[1]} alt="" class="preview-image" onerror={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} /> |
|
|
|
} |
|
|
|
</div> |
|
|
|
|
|
|
|
{/if} |
|
|
|
return { |
|
|
|
</div> |
|
|
|
kind: effectiveKind, |
|
|
|
{/if} |
|
|
|
pubkey: sessionManager.getCurrentPubkey() || '', |
|
|
|
|
|
|
|
created_at: Math.floor(Date.now() / 1000), |
|
|
|
|
|
|
|
tags: previewTags, |
|
|
|
|
|
|
|
content: previewContent, |
|
|
|
|
|
|
|
id: '', |
|
|
|
|
|
|
|
sig: '' |
|
|
|
|
|
|
|
} as NostrEvent; |
|
|
|
|
|
|
|
})()} |
|
|
|
|
|
|
|
{#if isParameterizedReplaceableKind(effectiveKind)} |
|
|
|
{#if isParameterizedReplaceableKind(effectiveKind)} |
|
|
|
{@const dTag = previewEvent.tags.find(t => t[0] === 'd' && t[1])} |
|
|
|
{@const dTag = previewEvent.tags.find(t => t[0] === 'd' && t[1])} |
|
|
|
{#if dTag} |
|
|
|
{#if dTag} |
|
|
|
@ -734,6 +792,8 @@ |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
<MediaAttachments event={previewEvent} /> |
|
|
|
<MediaAttachments event={previewEvent} /> |
|
|
|
<MarkdownRenderer content={previewContent} event={previewEvent} /> |
|
|
|
<MarkdownRenderer content={previewContent} event={previewEvent} /> |
|
|
|
|
|
|
|
{:else if content.trim() || uploadedFiles.length > 0} |
|
|
|
|
|
|
|
<p class="text-muted">Loading preview...</p> |
|
|
|
{:else} |
|
|
|
{:else} |
|
|
|
<p class="text-muted">No content to preview</p> |
|
|
|
<p class="text-muted">No content to preview</p> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
@ -1434,6 +1494,55 @@ |
|
|
|
padding: 1.5rem; |
|
|
|
padding: 1.5rem; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.preview-metadata { |
|
|
|
|
|
|
|
padding: 1rem; |
|
|
|
|
|
|
|
background: var(--fog-highlight, #f1f5f9); |
|
|
|
|
|
|
|
border: 1px solid var(--fog-border, #cbd5e1); |
|
|
|
|
|
|
|
border-radius: 0.375rem; |
|
|
|
|
|
|
|
margin-bottom: 1rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .preview-metadata { |
|
|
|
|
|
|
|
background: var(--fog-dark-highlight, #1e293b); |
|
|
|
|
|
|
|
border-color: var(--fog-dark-border, #475569); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.metadata-item { |
|
|
|
|
|
|
|
margin-bottom: 0.75rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.metadata-item:last-child { |
|
|
|
|
|
|
|
margin-bottom: 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.metadata-label { |
|
|
|
|
|
|
|
display: inline-block; |
|
|
|
|
|
|
|
min-width: 100px; |
|
|
|
|
|
|
|
color: var(--fog-text, #475569); |
|
|
|
|
|
|
|
font-weight: 500; |
|
|
|
|
|
|
|
margin-right: 0.5rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .metadata-label { |
|
|
|
|
|
|
|
color: var(--fog-dark-text, #cbd5e1); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.metadata-value { |
|
|
|
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .metadata-value { |
|
|
|
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.preview-image { |
|
|
|
|
|
|
|
max-width: 100%; |
|
|
|
|
|
|
|
max-height: 300px; |
|
|
|
|
|
|
|
border-radius: 0.375rem; |
|
|
|
|
|
|
|
margin-top: 0.5rem; |
|
|
|
|
|
|
|
display: block; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.d-tag-preview { |
|
|
|
.d-tag-preview { |
|
|
|
padding: 0.75rem; |
|
|
|
padding: 0.75rem; |
|
|
|
background: #f1f5f9; |
|
|
|
background: #f1f5f9; |
|
|
|
|