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.
649 lines
17 KiB
649 lines
17 KiB
<script lang="ts"> |
|
import { sessionManager } from '../../services/auth/session-manager.js'; |
|
import { signAndPublish } from '../../services/nostr/auth-handler.js'; |
|
import { relayManager } from '../../services/nostr/relay-manager.js'; |
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
import { cacheEvent } from '../../services/cache/event-cache.js'; |
|
import PublicationStatusModal from '../modals/PublicationStatusModal.svelte'; |
|
import { goto } from '$app/navigation'; |
|
import { KIND, isParameterizedReplaceableKind } from '../../types/kind-lookup.js'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
interface Props { |
|
isOpen: boolean; |
|
pubkey: string; |
|
onClose: () => void; |
|
} |
|
|
|
let { isOpen, pubkey, onClose }: Props = $props(); |
|
|
|
const PROFILE_EVENT_KINDS = [ |
|
{ kind: 0, name: 'Metadata (Profile)' }, |
|
{ kind: 3, name: 'Contacts' }, |
|
{ kind: 30315, name: 'User Status' }, |
|
{ kind: 10133, name: 'Payment Addresses' }, |
|
{ kind: 10002, name: 'Relay List' }, |
|
{ kind: 10432, name: 'Local Relays' }, |
|
{ kind: 10001, name: 'Pin List' }, |
|
{ kind: 10003, name: 'Bookmarks' }, |
|
{ kind: 10895, name: 'RSS Feed' }, |
|
{ kind: 10015, name: 'Interest List' }, |
|
{ kind: 10030, name: 'Emoji Set' }, |
|
{ kind: 30030, name: 'Emoji Pack' }, |
|
{ kind: 10000, name: 'Mute List' }, |
|
{ kind: 30008, name: 'Badges' }, |
|
{ kind: 30000, name: 'Follow Set' } |
|
]; |
|
|
|
let selectedKind = $state<number | null>(null); |
|
let editingEvent = $state<NostrEvent | null>(null); |
|
let content = $state(''); |
|
let tags = $state<string[][]>([]); |
|
let publishing = $state(false); |
|
let publicationModalOpen = $state(false); |
|
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null); |
|
|
|
async function selectKind(kind: number) { |
|
selectedKind = kind; |
|
|
|
// Load existing event(s) |
|
// For parameterized replaceable events (30000-39999), user can have multiple, so we load all |
|
// For other kinds, typically one per user |
|
try { |
|
const relays = relayManager.getProfileReadRelays(); |
|
const limit = isParameterizedReplaceableKind(kind) ? 50 : 1; // Load multiple for parameterized replaceable events |
|
const events = await nostrClient.fetchEvents( |
|
[{ kinds: [kind], authors: [pubkey], limit }], |
|
relays, |
|
{ useCache: true, cacheResults: true } |
|
); |
|
|
|
if (events.length > 0) { |
|
// Get newest version (or first if multiple allowed) |
|
editingEvent = events.sort((a, b) => b.created_at - a.created_at)[0]; |
|
content = editingEvent.content || ''; |
|
tags = [...editingEvent.tags]; |
|
} else { |
|
editingEvent = null; |
|
content = ''; |
|
tags = []; |
|
} |
|
} catch (error) { |
|
console.error('Error loading event:', error); |
|
editingEvent = null; |
|
content = ''; |
|
tags = []; |
|
} |
|
} |
|
|
|
function addTag() { |
|
tags = [...tags, ['', '']]; |
|
} |
|
|
|
function removeTag(index: number) { |
|
tags = tags.filter((_, i) => i !== index); |
|
} |
|
|
|
function updateTag(index: number, field: number, value: string) { |
|
const newTags = [...tags]; |
|
if (!newTags[index]) { |
|
newTags[index] = ['', '']; |
|
} |
|
newTags[index] = [...newTags[index]]; |
|
newTags[index][field] = value; |
|
while (newTags[index].length <= field) { |
|
newTags[index].push(''); |
|
} |
|
tags = newTags; |
|
} |
|
|
|
async function publish() { |
|
if (selectedKind === null) return; |
|
|
|
const session = sessionManager.getSession(); |
|
if (!session || session.pubkey !== pubkey) { |
|
alert('You can only edit your own profile events'); |
|
return; |
|
} |
|
|
|
publishing = true; |
|
|
|
try { |
|
// Always use a new timestamp for newly-published events |
|
// This ensures the event is considered "newer" and replaces older versions for replaceable events |
|
const created_at = Math.floor(Date.now() / 1000); |
|
|
|
const eventTemplate = { |
|
kind: selectedKind, |
|
pubkey: session.pubkey, |
|
created_at, |
|
tags: tags.filter(t => t[0] && t[1]), |
|
content |
|
}; |
|
|
|
const signedEvent = await session.signer(eventTemplate); |
|
await cacheEvent(signedEvent); |
|
|
|
const relays = relayManager.getPublishRelays( |
|
relayManager.getProfileReadRelays(), |
|
true |
|
); |
|
|
|
const results = await signAndPublish(eventTemplate, relays); |
|
publicationResults = results; |
|
publicationModalOpen = true; |
|
|
|
if (results.success.length > 0) { |
|
setTimeout(() => { |
|
goto(`/event/${signedEvent.id}`); |
|
}, 5000); |
|
} |
|
} catch (error) { |
|
console.error('Error publishing event:', error); |
|
publicationResults = { |
|
success: [], |
|
failed: [{ relay: 'Unknown', error: error instanceof Error ? error.message : 'Unknown error' }] |
|
}; |
|
publicationModalOpen = true; |
|
} finally { |
|
publishing = false; |
|
} |
|
} |
|
|
|
async function republishFromCache() { |
|
if (!publicationResults || selectedKind === null) return; |
|
|
|
publishing = true; |
|
try { |
|
const session = sessionManager.getSession(); |
|
if (!session) return; |
|
|
|
const relays = relayManager.getPublishRelays( |
|
relayManager.getProfileReadRelays(), |
|
true |
|
); |
|
|
|
// Always use a new timestamp for newly-published events |
|
const eventTemplate = { |
|
kind: selectedKind, |
|
pubkey: session.pubkey, |
|
created_at: Math.floor(Date.now() / 1000), |
|
tags: tags.filter(t => t[0] && t[1]), |
|
content |
|
}; |
|
|
|
const results = await signAndPublish(eventTemplate, relays); |
|
publicationResults = results; |
|
} catch (error) { |
|
console.error('Error republishing:', error); |
|
} finally { |
|
publishing = false; |
|
} |
|
} |
|
|
|
function closeForm() { |
|
selectedKind = null; |
|
editingEvent = null; |
|
content = ''; |
|
tags = []; |
|
} |
|
</script> |
|
|
|
{#if isOpen} |
|
<div |
|
class="panel-backdrop" |
|
onclick={onClose} |
|
onkeydown={(e) => { |
|
if (e.key === 'Enter' || e.key === ' ') { |
|
e.preventDefault(); |
|
onClose(); |
|
} |
|
}} |
|
role="button" |
|
tabindex="0" |
|
aria-label="Close panel" |
|
></div> |
|
<div class="profile-events-panel"> |
|
<div class="panel-header"> |
|
<h2 class="panel-title">Adjust Profile Events</h2> |
|
<button class="panel-close" onclick={onClose}>×</button> |
|
</div> |
|
|
|
<div class="panel-content"> |
|
{#if selectedKind === null} |
|
<div class="kind-list"> |
|
{#each PROFILE_EVENT_KINDS as { kind, name }} |
|
<button class="kind-button" onclick={() => selectKind(kind)}> |
|
{name} (Kind {kind}) |
|
</button> |
|
{/each} |
|
</div> |
|
{:else} |
|
<div class="edit-form"> |
|
<button class="back-button" onclick={closeForm}>← Back</button> |
|
<h3 class="form-title">Edit Kind {selectedKind}</h3> |
|
|
|
<div class="form-group"> |
|
<label for="content-textarea" class="form-label">Content</label> |
|
<textarea |
|
id="content-textarea" |
|
bind:value={content} |
|
class="content-input" |
|
rows="10" |
|
placeholder="Event content..." |
|
></textarea> |
|
</div> |
|
|
|
<div class="form-group"> |
|
<fieldset> |
|
<legend class="form-label">Tags</legend> |
|
<div class="tags-list"> |
|
{#each tags as tag, index (index)} |
|
<div class="tag-row"> |
|
<input |
|
type="text" |
|
value={tag[0] || ''} |
|
oninput={(e) => updateTag(index, 0, e.currentTarget.value)} |
|
placeholder="Tag name" |
|
class="tag-input" |
|
/> |
|
{#each tag.slice(1) as value, valueIndex} |
|
<input |
|
type="text" |
|
value={value || ''} |
|
oninput={(e) => updateTag(index, valueIndex + 1, e.currentTarget.value)} |
|
placeholder="Tag value" |
|
class="tag-input" |
|
/> |
|
{/each} |
|
<button class="tag-add-value" onclick={() => { |
|
const newTags = [...tags]; |
|
newTags[index] = [...newTags[index], '']; |
|
tags = newTags; |
|
}}>+</button> |
|
<button class="tag-remove" onclick={() => removeTag(index)}>×</button> |
|
</div> |
|
{/each} |
|
<button class="add-tag-button" onclick={addTag}>Add Tag</button> |
|
</div> |
|
</fieldset> |
|
</div> |
|
|
|
<div class="form-actions"> |
|
<button |
|
class="publish-button" |
|
onclick={publish} |
|
disabled={publishing} |
|
> |
|
{publishing ? 'Publishing...' : 'Publish'} |
|
</button> |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} /> |
|
|
|
{#if publicationResults && publicationResults.success.length === 0 && publicationResults.failed.length > 0} |
|
<div class="republish-section"> |
|
<p class="republish-text">All relays failed. You can attempt to republish from cache.</p> |
|
<button class="republish-button" onclick={republishFromCache} disabled={publishing}> |
|
Republish from Cache |
|
</button> |
|
</div> |
|
{/if} |
|
|
|
<style> |
|
.panel-backdrop { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: rgba(0, 0, 0, 0.5); |
|
z-index: 999; |
|
} |
|
|
|
.profile-events-panel { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
bottom: 0; |
|
width: min(500px, 90vw); |
|
background: var(--fog-post, #ffffff); |
|
border-right: 2px solid var(--fog-border, #cbd5e1); |
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2); |
|
z-index: 1000; |
|
display: flex; |
|
flex-direction: column; |
|
overflow: hidden; |
|
} |
|
|
|
:global(.dark) .profile-events-panel { |
|
background: var(--fog-dark-post, #1f2937); |
|
border-right-color: var(--fog-dark-border, #475569); |
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.panel-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 1rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
flex-shrink: 0; |
|
} |
|
|
|
:global(.dark) .panel-header { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.panel-title { |
|
margin: 0; |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .panel-title { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.panel-close { |
|
background: transparent; |
|
border: none; |
|
font-size: 1.5rem; |
|
line-height: 1; |
|
cursor: pointer; |
|
color: var(--fog-text-light, #9ca3af); |
|
padding: 0.25rem 0.5rem; |
|
border-radius: 0.25rem; |
|
} |
|
|
|
.panel-close:hover { |
|
background: var(--fog-highlight, #f3f4f6); |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .panel-close { |
|
color: var(--fog-dark-text-light, #6b7280); |
|
} |
|
|
|
:global(.dark) .panel-close:hover { |
|
background: var(--fog-dark-highlight, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.panel-content { |
|
overflow-y: auto; |
|
flex: 1; |
|
padding: 1rem; |
|
} |
|
|
|
.kind-list { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
} |
|
|
|
.kind-button { |
|
padding: 0.75rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-post, #ffffff); |
|
color: var(--fog-text, #1f2937); |
|
cursor: pointer; |
|
text-align: left; |
|
transition: all 0.2s; |
|
} |
|
|
|
:global(.dark) .kind-button { |
|
border-color: var(--fog-dark-border, #374151); |
|
background: var(--fog-dark-post, #1f2937); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.kind-button:hover { |
|
border-color: var(--fog-accent, #64748b); |
|
background: var(--fog-highlight, #f3f4f6); |
|
} |
|
|
|
:global(.dark) .kind-button:hover { |
|
border-color: var(--fog-dark-accent, #94a3b8); |
|
background: var(--fog-dark-highlight, #374151); |
|
} |
|
|
|
.edit-form { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 1rem; |
|
} |
|
|
|
.back-button { |
|
padding: 0.5rem 1rem; |
|
background: var(--fog-highlight, #f3f4f6); |
|
color: var(--fog-text, #1f2937); |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
align-self: flex-start; |
|
} |
|
|
|
:global(.dark) .back-button { |
|
background: var(--fog-dark-highlight, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
border-color: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.back-button:hover { |
|
background: var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .back-button:hover { |
|
background: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.form-title { |
|
margin: 0; |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .form-title { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.form-group { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
} |
|
|
|
.form-label { |
|
font-weight: 500; |
|
color: var(--fog-text, #1f2937); |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .form-label { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.content-input { |
|
padding: 0.75rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-post, #ffffff); |
|
color: var(--fog-text, #1f2937); |
|
font-size: 0.875rem; |
|
font-family: monospace; |
|
resize: vertical; |
|
} |
|
|
|
:global(.dark) .content-input { |
|
border-color: var(--fog-dark-border, #374151); |
|
background: var(--fog-dark-post, #1f2937); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.tags-list { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
} |
|
|
|
.tag-row { |
|
display: flex; |
|
gap: 0.5rem; |
|
align-items: center; |
|
} |
|
|
|
.tag-input { |
|
flex: 1; |
|
padding: 0.5rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-post, #ffffff); |
|
color: var(--fog-text, #1f2937); |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .tag-input { |
|
border-color: var(--fog-dark-border, #374151); |
|
background: var(--fog-dark-post, #1f2937); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.tag-add-value, |
|
.tag-remove { |
|
padding: 0.5rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-highlight, #f3f4f6); |
|
color: var(--fog-text, #1f2937); |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
min-width: 2rem; |
|
} |
|
|
|
:global(.dark) .tag-add-value, |
|
:global(.dark) .tag-remove { |
|
border-color: var(--fog-dark-border, #374151); |
|
background: var(--fog-dark-highlight, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.tag-add-value:hover, |
|
.tag-remove:hover { |
|
background: var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .tag-add-value:hover, |
|
:global(.dark) .tag-remove:hover { |
|
background: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.add-tag-button { |
|
padding: 0.5rem 1rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-highlight, #f3f4f6); |
|
color: var(--fog-text, #1f2937); |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
align-self: flex-start; |
|
} |
|
|
|
:global(.dark) .add-tag-button { |
|
border-color: var(--fog-dark-border, #374151); |
|
background: var(--fog-dark-highlight, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.add-tag-button:hover { |
|
background: var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .add-tag-button:hover { |
|
background: var(--fog-dark-border, #475569); |
|
} |
|
|
|
.form-actions { |
|
display: flex; |
|
gap: 0.5rem; |
|
} |
|
|
|
.publish-button { |
|
padding: 0.75rem 1.5rem; |
|
background: var(--fog-accent, #64748b); |
|
color: white; |
|
border: none; |
|
border-radius: 0.25rem; |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
font-weight: 500; |
|
} |
|
|
|
:global(.dark) .publish-button { |
|
background: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
.publish-button:hover:not(:disabled) { |
|
opacity: 0.9; |
|
} |
|
|
|
.publish-button:disabled { |
|
opacity: 0.6; |
|
cursor: not-allowed; |
|
} |
|
|
|
.republish-section { |
|
margin-top: 1rem; |
|
padding: 1rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
background: var(--fog-highlight, #f3f4f6); |
|
} |
|
|
|
:global(.dark) .republish-section { |
|
border-color: var(--fog-dark-border, #374151); |
|
background: var(--fog-dark-highlight, #374151); |
|
} |
|
|
|
.republish-text { |
|
margin: 0 0 0.5rem 0; |
|
color: var(--fog-text, #1f2937); |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .republish-text { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.republish-button { |
|
padding: 0.5rem 1rem; |
|
background: var(--fog-accent, #64748b); |
|
color: white; |
|
border: none; |
|
border-radius: 0.25rem; |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .republish-button { |
|
background: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
.republish-button:hover:not(:disabled) { |
|
opacity: 0.9; |
|
} |
|
|
|
.republish-button:disabled { |
|
opacity: 0.6; |
|
cursor: not-allowed; |
|
} |
|
</style>
|
|
|