Browse Source

bug-fixes

clone events
master
Silberengel 1 month ago
parent
commit
0e5bbf5393
  1. 94
      src/lib/components/EventMenu.svelte
  2. 6
      src/lib/components/content/EmojiPicker.svelte
  3. 7
      src/lib/components/content/GifPicker.svelte
  4. 7
      src/lib/components/content/PollCard.svelte
  5. 7
      src/lib/components/modals/EventJsonModal.svelte
  6. 8
      src/lib/components/modals/RelatedEventsModal.svelte
  7. 21
      src/lib/components/profile/ProfileMenu.svelte
  8. 26
      src/lib/modules/discussions/DiscussionVoteButtons.svelte
  9. 20
      src/lib/modules/reactions/FeedReactionButtons.svelte
  10. 6
      src/lib/services/nostr/config.ts
  11. 43
      src/lib/services/user-actions.ts
  12. 23
      src/routes/write/+page.svelte

94
src/lib/components/EventMenu.svelte

@ -20,6 +20,7 @@
import { KIND } from '../types/kind-lookup.js'; import { KIND } from '../types/kind-lookup.js';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Icon from './ui/Icon.svelte'; import Icon from './ui/Icon.svelte';
import { getEventLink } from '../services/event-links.js';
interface Props { interface Props {
event: NostrEvent; event: NostrEvent;
@ -243,16 +244,47 @@
closeMenu(); closeMenu();
} }
function viewEvent() {
closeMenu();
goto(getEventLink(event));
}
function cloneEvent() {
// Store event data in sessionStorage for the write page to pick up
const cloneData = {
kind: event.kind,
content: event.content,
tags: event.tags,
isClone: true
};
sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData));
closeMenu();
goto('/write');
}
async function broadcastEvent() { async function broadcastEvent() {
broadcasting = true; broadcasting = true;
closeMenu(); closeMenu();
try { try {
// Get all available relays for broadcasting // Get ALL available relays for maximum broadcasting
const relays = relayManager.getPublishRelays( // Start with all available relays (includes default, profile, and user inbox relays)
[...relayManager.getThreadReadRelays(), ...relayManager.getFeedReadRelays()], let allRelays = relayManager.getAllAvailableRelays();
true
); // Add thread publish relays (includes thread-specific relays)
allRelays = [...allRelays, ...relayManager.getThreadPublishRelays()];
// Add file metadata publish relays (includes GIF relays)
allRelays = [...allRelays, ...relayManager.getFileMetadataPublishRelays()];
// Add feed response relays
allRelays = [...allRelays, ...relayManager.getFeedResponseReadRelays()];
// Use getPublishRelays to ensure user outbox and local write relays are included
// This will also normalize, deduplicate, filter read-only relays, and filter blocked relays
const relays = relayManager.getPublishRelays(allRelays, true);
console.log(`[Broadcast] Broadcasting to ${relays.length} relays:`, relays);
const results = await nostrClient.publish(event, { relays }); const results = await nostrClient.publish(event, { relays });
publicationResults = results; publicationResults = results;
@ -284,14 +316,18 @@
} }
async function pinNote() { async function pinNote() {
await togglePin(event.id); const results = await togglePin(event.id);
publicationResults = results;
publicationModalOpen = true;
// Force state update // Force state update
stateUpdateTrigger++; stateUpdateTrigger++;
closeMenu(); closeMenu();
} }
async function bookmarkNote() { async function bookmarkNote() {
await toggleBookmark(event.id); const results = await toggleBookmark(event.id);
publicationResults = results;
publicationModalOpen = true;
// Force state update by re-checking bookmark status // Force state update by re-checking bookmark status
const newBookmarked = await isBookmarked(event.id); const newBookmarked = await isBookmarked(event.id);
bookmarkedState = newBookmarked; bookmarkedState = newBookmarked;
@ -390,6 +426,19 @@
class="menu-dropdown" class="menu-dropdown"
style="top: {menuPosition.top}px; right: {menuPosition.right}px;" style="top: {menuPosition.top}px; right: {menuPosition.right}px;"
> >
<!-- View actions -->
<button class="menu-item" onclick={viewEvent}>
<Icon name="eye" size={16} />
<span>View this note</span>
</button>
<button class="menu-item" onclick={viewJson}>
<Icon name="code" size={16} />
<span>View JSON</span>
</button>
<div class="menu-divider"></div>
<!-- Copy actions -->
<button class="menu-item" onclick={copyUserId}> <button class="menu-item" onclick={copyUserId}>
<Icon name="copy" size={16} /> <Icon name="copy" size={16} />
<span>Copy user ID</span> <span>Copy user ID</span>
@ -404,28 +453,36 @@
<span class="copied-indicator"></span> <span class="copied-indicator"></span>
{/if} {/if}
</button> </button>
<button class="menu-item" onclick={viewJson}>
<Icon name="code" size={16} /> <div class="menu-divider"></div>
<span>View JSON</span>
<!-- Share actions -->
<button class="menu-item" onclick={shareWithaitherboard}>
<Icon name="share" size={16} />
<span>Share with aitherboard</span>
{#if copied === 'share'}
<span class="copied-indicator"></span>
{/if}
</button> </button>
<!-- User-specific actions (logged in only) -->
{#if isLoggedIn} {#if isLoggedIn}
<div class="menu-divider"></div>
<button class="menu-item" onclick={viewRelatedEvents}> <button class="menu-item" onclick={viewRelatedEvents}>
<Icon name="search" size={16} /> <Icon name="search" size={16} />
<span>View your related events</span> <span>View your related events</span>
</button> </button>
{/if} <button class="menu-item" onclick={cloneEvent}>
<Icon name="edit" size={16} />
<span>Edit/Clone this event</span>
</button>
<button class="menu-item" onclick={broadcastEvent} disabled={broadcasting}> <button class="menu-item" onclick={broadcastEvent} disabled={broadcasting}>
<Icon name="radio" size={16} /> <Icon name="radio" size={16} />
<span>{broadcasting ? 'Broadcasting...' : 'Broadcast event'}</span> <span>{broadcasting ? 'Broadcasting...' : 'Broadcast event'}</span>
</button> </button>
<button class="menu-item" onclick={shareWithaitherboard}>
<Icon name="share" size={16} />
<span>Share with aitherboard</span>
{#if copied === 'share'}
<span class="copied-indicator"></span>
{/if} {/if}
</button>
<!-- Interaction actions (logged in only) -->
{#if isLoggedIn && onReply} {#if isLoggedIn && onReply}
<div class="menu-divider"></div> <div class="menu-divider"></div>
<button class="menu-item menu-item-reply" onclick={() => { onReply(); closeMenu(); }}> <button class="menu-item menu-item-reply" onclick={() => { onReply(); closeMenu(); }}>
@ -434,7 +491,9 @@
</button> </button>
{/if} {/if}
{#if isLoggedIn && showContentActions} {#if isLoggedIn && showContentActions}
{#if !onReply}
<div class="menu-divider"></div> <div class="menu-divider"></div>
{/if}
<button class="menu-item" onclick={pinNote} class:active={pinnedState}> <button class="menu-item" onclick={pinNote} class:active={pinnedState}>
<Icon name="plus" size={16} /> <Icon name="plus" size={16} />
<span>Pin note</span> <span>Pin note</span>
@ -458,6 +517,7 @@
</button> </button>
{/if} {/if}
<!-- Delete action (logged in and own event only) -->
{#if isLoggedIn && isOwnEvent} {#if isLoggedIn && isOwnEvent}
<div class="menu-divider"></div> <div class="menu-divider"></div>
<button class="menu-item menu-item-danger" onclick={confirmDelete} disabled={deleting}> <button class="menu-item menu-item-danger" onclick={confirmDelete} disabled={deleting}>

6
src/lib/components/content/EmojiPicker.svelte

@ -11,6 +11,7 @@
import { KIND } from '../../types/kind-lookup.js'; import { KIND } from '../../types/kind-lookup.js';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
import { cacheEvent } from '../../services/cache/event-cache.js'; import { cacheEvent } from '../../services/cache/event-cache.js';
import PublicationStatusModal from '../modals/PublicationStatusModal.svelte';
interface Props { interface Props {
open: boolean; open: boolean;
@ -29,6 +30,8 @@
let fileInput: HTMLInputElement | null = $state(null); let fileInput: HTMLInputElement | null = $state(null);
let shortcodeInput: HTMLInputElement | null = $state(null); let shortcodeInput: HTMLInputElement | null = $state(null);
let showUploadForm = $state(false); let showUploadForm = $state(false);
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
// Check if user is logged in // Check if user is logged in
let isLoggedIn = $derived(sessionManager.isLoggedIn()); let isLoggedIn = $derived(sessionManager.isLoggedIn());
@ -305,6 +308,8 @@
// Publish the event // Publish the event
const result = await signAndPublish(event, relays); const result = await signAndPublish(event, relays);
publicationResults = result;
publicationModalOpen = true;
if (result.success.length > 0) { if (result.success.length > 0) {
console.log(`[EmojiPicker] Published emoji set event for ${file.name} (shortcode: :${finalShortcode}:) to ${result.success.length} relay(s)`); console.log(`[EmojiPicker] Published emoji set event for ${file.name} (shortcode: :${finalShortcode}:) to ${result.success.length} relay(s)`);
@ -450,6 +455,7 @@
{/snippet} {/snippet}
</EmojiDrawer> </EmojiDrawer>
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style> <style>
.emoji-picker-wrapper { .emoji-picker-wrapper {

7
src/lib/components/content/GifPicker.svelte

@ -8,6 +8,7 @@
import { KIND } from '../../types/kind-lookup.js'; import { KIND } from '../../types/kind-lookup.js';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
import { cacheEvent } from '../../services/cache/event-cache.js'; import { cacheEvent } from '../../services/cache/event-cache.js';
import PublicationStatusModal from '../modals/PublicationStatusModal.svelte';
interface Props { interface Props {
open: boolean; open: boolean;
@ -40,6 +41,8 @@
image: '', image: '',
content: '' content: ''
}); });
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
// Check if user is logged in // Check if user is logged in
let isLoggedIn = $derived(sessionManager.isLoggedIn()); let isLoggedIn = $derived(sessionManager.isLoggedIn());
@ -365,6 +368,8 @@
// Publish the event // Publish the event
const result = await signAndPublish(event, relays); const result = await signAndPublish(event, relays);
publicationResults = result;
publicationModalOpen = true;
if (result.success.length > 0) { if (result.success.length > 0) {
console.log(`[GifPicker] Published file metadata event for ${file.name} to ${result.success.length} relay(s)`); console.log(`[GifPicker] Published file metadata event for ${file.name} to ${result.success.length} relay(s)`);
@ -619,6 +624,8 @@
</div> </div>
{/if} {/if}
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style> <style>
.drawer-backdrop { .drawer-backdrop {
position: fixed; position: fixed;

7
src/lib/components/content/PollCard.svelte

@ -6,6 +6,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js'; import { KIND } from '../../types/kind-lookup.js';
import PublicationStatusModal from '../modals/PublicationStatusModal.svelte';
interface Props { interface Props {
pollEvent: NostrEvent; // The poll event (kind 1068) pollEvent: NostrEvent; // The poll event (kind 1068)
@ -33,6 +34,8 @@
let submittingVote = $state(false); let submittingVote = $state(false);
let userVote = $state<string[]>([]); // Option IDs the current user has voted for let userVote = $state<string[]>([]); // Option IDs the current user has voted for
let hasVoted = $state(false); let hasVoted = $state(false);
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
// Parse poll event // Parse poll event
function parsePoll() { function parsePoll() {
@ -208,6 +211,8 @@
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
pubkey: session.pubkey pubkey: session.pubkey
}, relayUrls.length > 0 ? relayUrls : undefined); }, relayUrls.length > 0 ? relayUrls : undefined);
publicationResults = result;
publicationModalOpen = true;
if (result.success.length > 0) { if (result.success.length > 0) {
// Reload votes after successful publish // Reload votes after successful publish
@ -315,6 +320,8 @@
</div> </div>
</div> </div>
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style> <style>
.poll-card { .poll-card {
margin: 1rem 0; margin: 1rem 0;

7
src/lib/components/modals/EventJsonModal.svelte

@ -28,12 +28,6 @@
} }
} }
function selectAll() {
const textarea = document.querySelector('.json-textarea') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
}
}
</script> </script>
{#if open && event} {#if open && event}
@ -69,7 +63,6 @@
class="json-textarea" class="json-textarea"
readonly readonly
value={jsonText} value={jsonText}
onclick={selectAll}
></textarea> ></textarea>
</div> </div>

8
src/lib/components/modals/RelatedEventsModal.svelte

@ -108,13 +108,6 @@
} }
} }
function selectAll() {
const textarea = document.querySelector('.json-textarea') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
}
}
// Load related events when modal opens // Load related events when modal opens
$effect(() => { $effect(() => {
if (open && event && currentPubkey) { if (open && event && currentPubkey) {
@ -179,7 +172,6 @@
class="json-textarea" class="json-textarea"
readonly readonly
value={jsonText} value={jsonText}
onclick={selectAll}
></textarea> ></textarea>
{/if} {/if}
</div> </div>

21
src/lib/components/profile/ProfileMenu.svelte

@ -269,6 +269,20 @@
closeMenu(); closeMenu();
} }
function cloneEvent() {
if (!profileEvent) return;
// Store event data in sessionStorage for the write page to pick up
const cloneData = {
kind: profileEvent.kind,
content: profileEvent.content,
tags: profileEvent.tags,
isClone: true
};
sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData));
closeMenu();
goto('/write');
}
function seeBookmarks() { function seeBookmarks() {
if (onOpenBookmarks) { if (onOpenBookmarks) {
onOpenBookmarks(); onOpenBookmarks();
@ -349,6 +363,13 @@
<span class="menu-item-text">View Json</span> <span class="menu-item-text">View Json</span>
</button> </button>
{#if isLoggedIn && profileEvent}
<button class="menu-item" onclick={cloneEvent} role="menuitem">
<span class="menu-item-icon"></span>
<span class="menu-item-text">Edit/Clone this event</span>
</button>
{/if}
<div class="menu-divider"></div> <div class="menu-divider"></div>
<button class="menu-item" onclick={shareWithAitherboard} role="menuitem"> <button class="menu-item" onclick={shareWithAitherboard} role="menuitem">

26
src/lib/modules/discussions/DiscussionVoteButtons.svelte

@ -9,6 +9,7 @@
import { KIND } from '../../types/kind-lookup.js'; import { KIND } from '../../types/kind-lookup.js';
import VoteCount from '../../components/content/VoteCount.svelte'; import VoteCount from '../../components/content/VoteCount.svelte';
import { getCachedReactionsForEvents } from '../../services/cache/event-cache.js'; import { getCachedReactionsForEvents } from '../../services/cache/event-cache.js';
import PublicationStatusModal from '../../components/modals/PublicationStatusModal.svelte';
interface Props { interface Props {
event: NostrEvent; event: NostrEvent;
@ -28,6 +29,8 @@
let processingUpdate = $state(false); let processingUpdate = $state(false);
let updateDebounceTimer: ReturnType<typeof setTimeout> | null = null; let updateDebounceTimer: ReturnType<typeof setTimeout> | null = null;
let initialLoadComplete = $state(false); // Track when initial load is done let initialLoadComplete = $state(false); // Track when initial load is done
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
// Count upvotes and downvotes // Count upvotes and downvotes
let upvotes = $derived.by(() => { let upvotes = $derived.by(() => {
@ -419,8 +422,10 @@
content: '' content: ''
}; };
const config = nostrClient.getConfig(); const relays = relayManager.getReactionPublishRelays();
await signAndPublish(deletionEvent, [...config.defaultRelays]); const results = await signAndPublish(deletionEvent, relays);
publicationResults = results;
publicationModalOpen = true;
// Remove from map immediately so counts update // Remove from map immediately so counts update
allReactionsMap.delete(reactionIdToDelete); allReactionsMap.delete(reactionIdToDelete);
@ -454,8 +459,10 @@
content: '' content: ''
}; };
const config = nostrClient.getConfig(); const relays = relayManager.getReactionPublishRelays();
await signAndPublish(deletionEvent, [...config.defaultRelays]); const results = await signAndPublish(deletionEvent, relays);
publicationResults = results;
publicationModalOpen = true;
// Remove from map immediately so counts update // Remove from map immediately so counts update
// Reassign map to trigger reactivity in Svelte 5 // Reassign map to trigger reactivity in Svelte 5
@ -486,13 +493,14 @@
content content
}; };
const config = nostrClient.getConfig();
// Sign the event first to get the ID // Sign the event first to get the ID
const signedEvent = await sessionManager.signEvent(reactionEvent); const signedEvent = await sessionManager.signEvent(reactionEvent);
// Publish the signed event // Publish the signed event using reaction publish relays (filters read-only relays)
await nostrClient.publish(signedEvent, { relays: [...config.defaultRelays] }); const relays = relayManager.getReactionPublishRelays();
const results = await nostrClient.publish(signedEvent, { relays });
publicationResults = results;
publicationModalOpen = true;
// Update local state immediately for instant UI feedback // Update local state immediately for instant UI feedback
userReaction = content; userReaction = content;
@ -536,6 +544,8 @@
/> />
</div> </div>
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style> <style>
.discussion-vote-buttons { .discussion-vote-buttons {
margin-top: 0.5rem; margin-top: 0.5rem;

20
src/lib/modules/reactions/FeedReactionButtons.svelte

@ -13,6 +13,7 @@
import { shouldIncludeClientTag } from '../../services/client-tag-preference.js'; import { shouldIncludeClientTag } from '../../services/client-tag-preference.js';
import Icon from '../../components/ui/Icon.svelte'; import Icon from '../../components/ui/Icon.svelte';
import { getCachedReactionsForEvents } from '../../services/cache/event-cache.js'; import { getCachedReactionsForEvents } from '../../services/cache/event-cache.js';
import PublicationStatusModal from '../../components/modals/PublicationStatusModal.svelte';
interface Props { interface Props {
event: NostrEvent; event: NostrEvent;
@ -37,6 +38,8 @@
let isMounted = $state(false); let isMounted = $state(false);
let processingUpdate = $state(false); let processingUpdate = $state(false);
let updateDebounceTimer: ReturnType<typeof setTimeout> | null = null; let updateDebounceTimer: ReturnType<typeof setTimeout> | null = null;
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
onMount(() => { onMount(() => {
// Set lastEventId immediately to prevent $effect from running during mount // Set lastEventId immediately to prevent $effect from running during mount
@ -404,8 +407,10 @@
content: '' content: ''
}; };
const config = nostrClient.getConfig(); const relays = relayManager.getReactionPublishRelays();
await signAndPublish(deletionEvent, [...config.defaultRelays]); const results = await signAndPublish(deletionEvent, relays);
publicationResults = results;
publicationModalOpen = true;
// Update local state // Update local state
userReaction = null; userReaction = null;
@ -463,13 +468,14 @@
content content
}; };
const config = nostrClient.getConfig();
// Sign the event first to get the ID // Sign the event first to get the ID
const signedEvent = await sessionManager.signEvent(reactionEvent); const signedEvent = await sessionManager.signEvent(reactionEvent);
// Publish the signed event // Publish the signed event using reaction publish relays (filters read-only relays)
await nostrClient.publish(signedEvent, { relays: [...config.defaultRelays] }); const relays = relayManager.getReactionPublishRelays();
const results = await nostrClient.publish(signedEvent, { relays });
publicationResults = results;
publicationModalOpen = true;
// Update local state with the new reaction // Update local state with the new reaction
userReaction = content; userReaction = content;
@ -657,6 +663,8 @@
{/each} {/each}
</div> </div>
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style> <style>
.Feed-reaction-buttons { .Feed-reaction-buttons {
margin-top: 0.5rem; margin-top: 0.5rem;

6
src/lib/services/nostr/config.ts

@ -29,6 +29,10 @@ const GIF_RELAYS = [
"wss://thecitadel.nostr1.com" "wss://thecitadel.nostr1.com"
]; ];
const READ_ONLY_RELAYS = [
'wss://aggr.nostr.land' // Read-only aggregator
];
const RELAY_TIMEOUT = 10000; const RELAY_TIMEOUT = 10000;
// Fetch limits // Fetch limits
@ -53,6 +57,7 @@ export interface NostrConfig {
threadPublishRelays: string[]; threadPublishRelays: string[];
relayTimeout: number; relayTimeout: number;
gifRelays: string[]; gifRelays: string[];
readOnlyRelays: string[];
// Fetch limits // Fetch limits
feedLimit: number; feedLimit: number;
singleEventLimit: number; singleEventLimit: number;
@ -92,6 +97,7 @@ export function getConfig(): NostrConfig {
threadPublishRelays: THREAD_PUBLISH_RELAYS, threadPublishRelays: THREAD_PUBLISH_RELAYS,
relayTimeout: RELAY_TIMEOUT, relayTimeout: RELAY_TIMEOUT,
gifRelays: GIF_RELAYS, gifRelays: GIF_RELAYS,
readOnlyRelays: READ_ONLY_RELAYS,
// Fetch limits // Fetch limits
feedLimit: parseIntEnv(import.meta.env.VITE_FEED_LIMIT, FEED_LIMIT, 1), feedLimit: parseIntEnv(import.meta.env.VITE_FEED_LIMIT, FEED_LIMIT, 1),
singleEventLimit: SINGLE_EVENT_LIMIT, singleEventLimit: SINGLE_EVENT_LIMIT,

43
src/lib/services/user-actions.ts

@ -159,8 +159,9 @@ export function isHighlighted(eventId: string): boolean {
/** /**
* Toggle pin status of an event * Toggle pin status of an event
* Publishes kind 10001 list event (pins are stored in cache and on relays only) * Publishes kind 10001 list event (pins are stored in cache and on relays only)
* Returns publication results
*/ */
export async function togglePin(eventId: string): Promise<boolean> { export async function togglePin(eventId: string): Promise<{ success: string[]; failed: Array<{ relay: string; error: string }> }> {
try { try {
const session = sessionManager.getSession(); const session = sessionManager.getSession();
if (!session) { if (!session) {
@ -179,27 +180,26 @@ export async function togglePin(eventId: string): Promise<boolean> {
} }
// Publish updated pin list event // Publish updated pin list event
await publishPinList(Array.from(currentPins)); const result = await publishPinList(Array.from(currentPins));
// Invalidate cache so next read gets fresh data // Invalidate cache so next read gets fresh data
invalidatePinCache(); invalidatePinCache();
return !isCurrentlyPinned; return result;
} catch (error) { } catch (error) {
console.error('Failed to toggle pin:', error); console.error('Failed to toggle pin:', error);
// Return current state on error return { success: [], failed: [{ relay: 'unknown', error: error instanceof Error ? error.message : String(error) }] };
const currentPins = await getPinnedEvents();
return currentPins.has(eventId);
} }
} }
/** /**
* Publish pin list event (kind 10001) * Publish pin list event (kind 10001)
* Returns publication results
*/ */
async function publishPinList(eventIds: string[]): Promise<void> { async function publishPinList(eventIds: string[]): Promise<{ success: string[]; failed: Array<{ relay: string; error: string }> }> {
try { try {
const session = sessionManager.getSession(); const session = sessionManager.getSession();
if (!session) return; if (!session) return { success: [], failed: [] };
// Deduplicate input eventIds first // Deduplicate input eventIds first
const deduplicatedEventIds = Array.from(new Set(eventIds)); const deduplicatedEventIds = Array.from(new Set(eventIds));
@ -236,7 +236,7 @@ async function publishPinList(eventIds: string[]): Promise<void> {
const removedEventIds = [...existingEventIds].filter(id => !deduplicatedEventIds.includes(id)); const removedEventIds = [...existingEventIds].filter(id => !deduplicatedEventIds.includes(id));
if (newEventIds.length === 0 && removedEventIds.length === 0 && existingLists.length > 0) { if (newEventIds.length === 0 && removedEventIds.length === 0 && existingLists.length > 0) {
return; // No changes, cancel operation return { success: [], failed: [] }; // No changes, cancel operation
} }
// Build final tags: preserve all a-tags, add/update e-tags // Build final tags: preserve all a-tags, add/update e-tags
@ -293,17 +293,19 @@ async function publishPinList(eventIds: string[]): Promise<void> {
// Publish to write relays // Publish to write relays
const writeRelays = relayManager.getPublishRelays(relays, true); const writeRelays = relayManager.getPublishRelays(relays, true);
await signAndPublish(listEvent, writeRelays); return await signAndPublish(listEvent, writeRelays);
} catch (error) { } catch (error) {
console.error('Failed to publish pin list:', error); console.error('Failed to publish pin list:', error);
return { success: [], failed: [{ relay: 'unknown', error: error instanceof Error ? error.message : String(error) }] };
} }
} }
/** /**
* Toggle bookmark status of an event * Toggle bookmark status of an event
* Publishes kind 10003 list event (bookmarks are stored in cache and on relays only) * Publishes kind 10003 list event (bookmarks are stored in cache and on relays only)
* Returns publication results
*/ */
export async function toggleBookmark(eventId: string): Promise<boolean> { export async function toggleBookmark(eventId: string): Promise<{ success: string[]; failed: Array<{ relay: string; error: string }> }> {
try { try {
const session = sessionManager.getSession(); const session = sessionManager.getSession();
if (!session) { if (!session) {
@ -322,27 +324,26 @@ export async function toggleBookmark(eventId: string): Promise<boolean> {
} }
// Publish updated bookmark list event // Publish updated bookmark list event
await publishBookmarkList(Array.from(currentBookmarks)); const result = await publishBookmarkList(Array.from(currentBookmarks));
// Invalidate cache so next read gets fresh data // Invalidate cache so next read gets fresh data
invalidateBookmarkCache(); invalidateBookmarkCache();
return !isCurrentlyBookmarked; return result;
} catch (error) { } catch (error) {
console.error('Failed to toggle bookmark:', error); console.error('Failed to toggle bookmark:', error);
// Return current state on error return { success: [], failed: [{ relay: 'unknown', error: error instanceof Error ? error.message : String(error) }] };
const currentBookmarks = await getBookmarkedEvents();
return currentBookmarks.has(eventId);
} }
} }
/** /**
* Publish bookmark list event (kind 10003) * Publish bookmark list event (kind 10003)
* Returns publication results
*/ */
async function publishBookmarkList(eventIds: string[]): Promise<void> { async function publishBookmarkList(eventIds: string[]): Promise<{ success: string[]; failed: Array<{ relay: string; error: string }> }> {
try { try {
const session = sessionManager.getSession(); const session = sessionManager.getSession();
if (!session) return; if (!session) return { success: [], failed: [] };
// Deduplicate input eventIds first // Deduplicate input eventIds first
const deduplicatedEventIds = Array.from(new Set(eventIds)); const deduplicatedEventIds = Array.from(new Set(eventIds));
@ -379,7 +380,7 @@ async function publishBookmarkList(eventIds: string[]): Promise<void> {
const removedEventIds = [...existingEventIds].filter(id => !deduplicatedEventIds.includes(id)); const removedEventIds = [...existingEventIds].filter(id => !deduplicatedEventIds.includes(id));
if (newEventIds.length === 0 && removedEventIds.length === 0 && existingLists.length > 0) { if (newEventIds.length === 0 && removedEventIds.length === 0 && existingLists.length > 0) {
return; // No changes, cancel operation return { success: [], failed: [] }; // No changes, cancel operation
} }
// Build final tags: preserve all a-tags, add/update e-tags // Build final tags: preserve all a-tags, add/update e-tags
@ -436,9 +437,11 @@ async function publishBookmarkList(eventIds: string[]): Promise<void> {
// Publish to write relays // Publish to write relays
const writeRelays = relayManager.getPublishRelays(relays, true); const writeRelays = relayManager.getPublishRelays(relays, true);
await signAndPublish(listEvent, writeRelays); const result = await signAndPublish(listEvent, writeRelays);
return result;
} catch (error) { } catch (error) {
console.error('Failed to publish bookmark list:', error); console.error('Failed to publish bookmark list:', error);
return { success: [], failed: [{ relay: 'unknown', error: error instanceof Error ? error.message : String(error) }] };
} }
} }

23
src/routes/write/+page.svelte

@ -21,6 +21,7 @@
}); });
let initialContent = $state<string | null>(null); let initialContent = $state<string | null>(null);
let initialTags = $state<string[][] | null>(null); let initialTags = $state<string[][] | null>(null);
let isCloneMode = $state(false);
// Subscribe to session changes to reactively update login status // Subscribe to session changes to reactively update login status
let currentSession = $state(sessionManager.session.value); let currentSession = $state(sessionManager.session.value);
@ -46,7 +47,22 @@
} }
} }
// Check for highlight data in sessionStorage // Check for clone/edit event data in sessionStorage (takes priority)
const cloneDataStr = sessionStorage.getItem('aitherboard_cloneEvent');
if (cloneDataStr) {
try {
const cloneData = JSON.parse(cloneDataStr);
initialKind = cloneData.kind || null;
initialContent = cloneData.content || null;
initialTags = cloneData.tags || null;
isCloneMode = cloneData.isClone === true;
// Clear sessionStorage after reading
sessionStorage.removeItem('aitherboard_cloneEvent');
} catch (error) {
console.error('Error parsing clone event data:', error);
}
} else {
// Check for highlight data in sessionStorage (fallback)
const highlightDataStr = sessionStorage.getItem('aitherboard_highlightData'); const highlightDataStr = sessionStorage.getItem('aitherboard_highlightData');
if (highlightDataStr) { if (highlightDataStr) {
try { try {
@ -59,6 +75,7 @@
console.error('Error parsing highlight data:', error); console.error('Error parsing highlight data:', error);
} }
} }
}
}); });
</script> </script>
@ -66,7 +83,9 @@
<main class="container mx-auto px-4 py-8"> <main class="container mx-auto px-4 py-8">
<div class="write-page"> <div class="write-page">
<h1 class="font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono" style="font-size: 1.5em;">/Write</h1> <h1 class="font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono" style="font-size: 1.5em;">
{isCloneMode ? '/Write: Edit or Clone an event' : '/Write'}
</h1>
{#if !isLoggedIn} {#if !isLoggedIn}
<div class="login-prompt"> <div class="login-prompt">

Loading…
Cancel
Save