Browse Source

bug-fixes

master
Silberengel 1 month ago
parent
commit
d91198d8bb
  1. 431
      src/lib/components/EventMenu.svelte
  2. 214
      src/lib/components/modals/EventJsonModal.svelte
  3. 4
      src/lib/modules/comments/Comment.svelte
  4. 9
      src/lib/modules/feed/FeedPost.svelte
  5. 4
      src/lib/modules/feed/Reply.svelte
  6. 4
      src/lib/modules/feed/ZapReceiptReply.svelte
  7. 6
      src/lib/modules/threads/ThreadCard.svelte
  8. 57
      src/lib/services/event-menu-store.ts
  9. 140
      src/lib/services/user-actions.ts
  10. 3
      src/lib/types/kind-lookup.ts

431
src/lib/components/EventMenu.svelte

@ -0,0 +1,431 @@ @@ -0,0 +1,431 @@
<script lang="ts">
import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '../types/nostr.js';
import { nostrClient } from '../services/nostr/nostr-client.js';
import { relayManager } from '../services/nostr/relay-manager.js';
import EventJsonModal from './modals/EventJsonModal.svelte';
import PublicationStatusModal from './modals/PublicationStatusModal.svelte';
import {
isPinned,
isBookmarked,
isHighlighted,
togglePin,
toggleBookmark,
toggleHighlight
} from '../services/user-actions.js';
import { eventMenuStore } from '../services/event-menu-store.js';
interface Props {
event: NostrEvent;
showContentActions?: boolean; // Show pin/bookmark/highlight for notes with content
}
let { event, showContentActions = false }: Props = $props();
let menuOpen = $state(false);
let jsonModalOpen = $state(false);
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
let broadcasting = $state(false);
let copied = $state<string | null>(null);
let menuButtonElement: HTMLButtonElement | null = $state(null);
let menuDropdownElement: HTMLDivElement | null = $state(null);
let menuPosition = $state({ top: 0, right: 0 });
// Unique ID for this menu instance
let menuId = $derived(event.id);
// Check if this is a note with content (kind 1 or kind 11)
let isContentNote = $derived(event.kind === 1 || event.kind === 11);
// Track pin/bookmark/highlight state
let pinnedState = $state(false);
let bookmarkedState = $state(false);
let highlightedState = $state(false);
// Update state when event changes
$effect(() => {
pinnedState = isPinned(event.id);
bookmarkedState = isBookmarked(event.id);
highlightedState = isHighlighted(event.id);
});
function toggleMenu() {
if (menuOpen) {
closeMenu();
} else {
openMenu();
}
}
function openMenu() {
menuOpen = true;
// Register this menu as open - this will close any other open menu
eventMenuStore.openMenu(menuId, closeMenu);
// Position menu after opening
requestAnimationFrame(() => {
positionMenu();
});
}
function positionMenu() {
if (!menuButtonElement || !menuDropdownElement) return;
const buttonRect = menuButtonElement.getBoundingClientRect();
// Calculate position - align right edge of dropdown with right edge of button
// Position below button by default
const top = buttonRect.bottom + 4;
const right = window.innerWidth - buttonRect.right;
menuPosition = { top, right };
// Get actual dimensions after rendering
requestAnimationFrame(() => {
if (!menuDropdownElement) return;
const dropdownRect = menuDropdownElement.getBoundingClientRect();
// Adjust if menu would go off screen at bottom
if (dropdownRect.bottom > window.innerHeight) {
// Position above button if there's not enough space below
const spaceAbove = buttonRect.top;
const spaceBelow = window.innerHeight - buttonRect.bottom;
if (spaceAbove > spaceBelow) {
menuPosition.top = buttonRect.top - dropdownRect.height - 4;
}
}
// Adjust if menu would go off screen to the left
if (dropdownRect.left < 0) {
menuPosition.right = window.innerWidth - buttonRect.left;
}
});
}
function closeMenu() {
menuOpen = false;
eventMenuStore.closeMenu(menuId);
}
// Close menu when clicking outside
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement;
if (!target.closest('.event-menu-container') && !target.closest('.menu-dropdown')) {
eventMenuStore.closeCurrentMenu();
}
}
$effect(() => {
if (menuOpen) {
document.addEventListener('click', handleClickOutside);
window.addEventListener('scroll', closeMenu, true);
window.addEventListener('resize', positionMenu);
// Reposition on scroll to keep menu aligned
const handleScroll = () => {
if (menuOpen) {
positionMenu();
}
};
window.addEventListener('scroll', handleScroll, true);
return () => {
document.removeEventListener('click', handleClickOutside);
window.removeEventListener('scroll', closeMenu, true);
window.removeEventListener('resize', positionMenu);
window.removeEventListener('scroll', handleScroll, true);
};
}
});
async function copyUserId() {
try {
const npub = nip19.npubEncode(event.pubkey);
await navigator.clipboard.writeText(npub);
copied = 'npub';
setTimeout(() => {
copied = null;
}, 2000);
closeMenu();
} catch (error) {
console.error('Failed to copy user ID:', error);
}
}
async function copyEventId() {
try {
const nevent = nip19.neventEncode({
id: event.id,
relays: []
});
await navigator.clipboard.writeText(nevent);
copied = 'nevent';
setTimeout(() => {
copied = null;
}, 2000);
closeMenu();
} catch (error) {
console.error('Failed to copy event ID:', error);
}
}
function viewJson() {
jsonModalOpen = true;
closeMenu();
}
async function broadcastEvent() {
broadcasting = true;
closeMenu();
try {
// Get all available relays for broadcasting
const relays = relayManager.getPublishRelays(
[...relayManager.getThreadReadRelays(), ...relayManager.getFeedReadRelays()],
true
);
const results = await nostrClient.publish(event, { relays });
publicationResults = results;
publicationModalOpen = true;
} catch (error) {
console.error('Error broadcasting event:', error);
publicationResults = {
success: [],
failed: [{ relay: 'Unknown', error: error instanceof Error ? error.message : 'Unknown error' }]
};
publicationModalOpen = true;
} finally {
broadcasting = false;
}
}
async function shareWithAitherboard() {
try {
const url = `${window.location.origin}/thread/${event.id}`;
await navigator.clipboard.writeText(url);
copied = 'share';
setTimeout(() => {
copied = null;
}, 2000);
closeMenu();
} catch (error) {
console.error('Failed to copy share URL:', error);
}
}
function pinNote() {
pinnedState = togglePin(event.id);
closeMenu();
}
function bookmarkNote() {
bookmarkedState = toggleBookmark(event.id);
closeMenu();
}
function highlightNote() {
highlightedState = toggleHighlight(event.id);
closeMenu();
}
</script>
<div class="event-menu-container">
<button
bind:this={menuButtonElement}
class="menu-button"
onclick={toggleMenu}
aria-label="Event menu"
aria-expanded={menuOpen}
>
<span class="menu-icon"></span>
</button>
{#if menuOpen}
<div
bind:this={menuDropdownElement}
class="menu-dropdown"
style="top: {menuPosition.top}px; right: {menuPosition.right}px;"
>
<button class="menu-item" onclick={copyUserId}>
Copy user ID
{#if copied === 'npub'}
<span class="copied-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={copyEventId}>
Copy event ID
{#if copied === 'nevent'}
<span class="copied-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={viewJson}>
View JSON
</button>
<button class="menu-item" onclick={broadcastEvent} disabled={broadcasting}>
{broadcasting ? 'Broadcasting...' : 'Broadcast event'}
</button>
<button class="menu-item" onclick={shareWithAitherboard}>
Share with aitherboard
{#if copied === 'share'}
<span class="copied-indicator"></span>
{/if}
</button>
{#if showContentActions && isContentNote}
<div class="menu-divider"></div>
<button class="menu-item" onclick={pinNote} class:active={pinnedState}>
Pin note
{#if pinnedState}
<span class="action-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={bookmarkNote} class:active={bookmarkedState}>
Bookmark note
{#if bookmarkedState}
<span class="action-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={highlightNote} class:active={highlightedState}>
Highlight note
{#if highlightedState}
<span class="action-indicator"></span>
{/if}
</button>
{/if}
</div>
{/if}
</div>
<EventJsonModal bind:open={jsonModalOpen} bind:event />
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style>
.event-menu-container {
position: relative;
display: inline-block;
/* Ensure menu can overflow container */
overflow: visible;
}
.menu-button {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.5rem;
color: var(--fog-text-light, #9ca3af);
font-size: 1.25rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
}
.menu-button:hover {
background: var(--fog-highlight, #f3f4f6);
color: var(--fog-text, #1f2937);
}
:global(.dark) .menu-button {
color: var(--fog-dark-text-light, #6b7280);
}
:global(.dark) .menu-button:hover {
background: var(--fog-dark-highlight, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
.menu-icon {
user-select: none;
transform: rotate(90deg);
display: inline-block;
}
.menu-dropdown {
position: fixed;
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
z-index: 1000;
overflow: visible;
}
:global(.dark) .menu-dropdown {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.5rem 0.75rem;
background: none;
border: none;
text-align: left;
cursor: pointer;
font-size: 0.875rem;
color: var(--fog-text, #1f2937);
transition: background-color 0.2s;
}
.menu-item:hover:not(:disabled) {
background: var(--fog-highlight, #f3f4f6);
}
.menu-item:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.menu-item.active {
background: var(--fog-highlight, #f3f4f6);
font-weight: 500;
}
:global(.dark) .menu-item {
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .menu-item:hover:not(:disabled) {
background: var(--fog-dark-highlight, #374151);
}
:global(.dark) .menu-item.active {
background: var(--fog-dark-highlight, #374151);
}
.action-indicator {
color: var(--fog-accent, #64748b);
font-weight: 600;
margin-left: 0.5rem;
}
:global(.dark) .action-indicator {
color: var(--fog-dark-accent, #94a3b8);
}
.menu-divider {
height: 1px;
background: var(--fog-border, #e5e7eb);
margin: 0.25rem 0;
}
:global(.dark) .menu-divider {
background: var(--fog-dark-border, #374151);
}
.copied-indicator {
color: var(--fog-accent, #64748b);
font-weight: 600;
margin-left: 0.5rem;
}
:global(.dark) .copied-indicator {
color: var(--fog-dark-accent, #94a3b8);
}
</style>

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

@ -0,0 +1,214 @@ @@ -0,0 +1,214 @@
<script lang="ts">
import type { NostrEvent } from '../../types/nostr.js';
interface Props {
open?: boolean;
event?: NostrEvent | null;
}
let { open = $bindable(false), event = $bindable(null) }: Props = $props();
let jsonText = $derived(event ? JSON.stringify(event, null, 2) : '');
let copied = $state(false);
function close() {
open = false;
}
async function copyJson() {
if (!jsonText) return;
try {
await navigator.clipboard.writeText(jsonText);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
} catch (error) {
console.error('Failed to copy JSON:', error);
}
}
function selectAll() {
const textarea = document.querySelector('.json-textarea') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
}
}
</script>
{#if open && event}
<div
class="modal-overlay"
onclick={close}
onkeydown={(e) => e.key === 'Escape' && close()}
role="dialog"
aria-modal="true"
tabindex="-1"
>
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<h2>Event JSON</h2>
<button onclick={close} class="close-button">×</button>
</div>
<div class="modal-body">
<textarea
class="json-textarea"
readonly
value={jsonText}
onclick={selectAll}
></textarea>
</div>
<div class="modal-footer">
<button onclick={copyJson} class="copy-button">
{copied ? 'Copied!' : 'Copy'}
</button>
<button onclick={close}>Close</button>
</div>
</div>
</div>
{/if}
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 8px;
max-width: 800px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
:global(.dark) .modal-content {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .modal-header {
border-bottom-color: var(--fog-dark-border, #374151);
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .modal-header h2 {
color: var(--fog-dark-text, #f1f5f9);
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .close-button {
color: var(--fog-dark-text, #f1f5f9);
}
.modal-body {
padding: 1rem;
flex: 1;
overflow: auto;
}
.json-textarea {
width: 100%;
min-height: 400px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
padding: 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 4px;
background: var(--fog-bg, #ffffff);
color: var(--fog-text, #1f2937);
resize: vertical;
}
:global(.dark) .json-textarea {
background: var(--fog-dark-bg, #0f172a);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
.modal-footer {
padding: 1rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
:global(.dark) .modal-footer {
border-top-color: var(--fog-dark-border, #374151);
}
.modal-footer button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 0.875rem;
}
.copy-button {
background: var(--fog-accent, #64748b);
color: white;
}
.copy-button:hover {
background: var(--fog-accent-dark, #475569);
}
.modal-footer button:not(.copy-button) {
background: var(--fog-border, #e5e7eb);
color: var(--fog-text, #1f2937);
}
.modal-footer button:not(.copy-button):hover {
background: var(--fog-highlight, #f3f4f6);
}
:global(.dark) .modal-footer button:not(.copy-button) {
background: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .modal-footer button:not(.copy-button):hover {
background: var(--fog-dark-highlight, #374151);
}
</style>

4
src/lib/modules/comments/Comment.svelte

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte';
import ReplyContext from '../../components/content/ReplyContext.svelte';
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte';
import EventMenu from '../../components/EventMenu.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
@ -85,6 +86,9 @@ @@ -85,6 +86,9 @@
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
<div class="ml-auto">
<EventMenu event={comment} />
</div>
</div>
<div class="comment-content mb-2">

9
src/lib/modules/feed/FeedPost.svelte

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
import ReplyContext from '../../components/content/ReplyContext.svelte';
import QuotedContext from '../../components/content/QuotedContext.svelte';
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte';
import EventMenu from '../../components/EventMenu.svelte';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
@ -360,7 +361,10 @@ @@ -360,7 +361,10 @@
<h3 class="text-lg font-semibold">
<a href="/thread/{post.id}">{getTitle()}</a>
</h3>
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<div class="flex items-center gap-2">
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<EventMenu event={post} showContentActions={true} />
</div>
</div>
<div class="mb-2 flex items-center gap-2 flex-wrap">
@ -437,6 +441,9 @@ @@ -437,6 +441,9 @@
{/each}
{/if}
{/if}
<div class="ml-auto">
<EventMenu event={post} showContentActions={true} />
</div>
</div>
<div class="post-content mb-2">

4
src/lib/modules/feed/Reply.svelte

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte';
import ZapButton from '../zaps/ZapButton.svelte';
import ZapReceipt from '../zaps/ZapReceipt.svelte';
import EventMenu from '../../components/EventMenu.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
@ -90,6 +91,9 @@ @@ -90,6 +91,9 @@
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
<div class="ml-auto">
<EventMenu event={reply} showContentActions={reply.kind === 1} />
</div>
</div>
<div class="reply-content mb-2">

4
src/lib/modules/feed/ZapReceiptReply.svelte

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
<script lang="ts">
import ProfileBadge from '../../components/layout/ProfileBadge.svelte';
import ReplyContext from '../../components/content/ReplyContext.svelte';
import EventMenu from '../../components/EventMenu.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
@ -98,6 +99,9 @@ @@ -98,6 +99,9 @@
<span class="text-lg"></span>
<span class="text-sm font-semibold">{getAmount().toLocaleString()} sats</span>
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<div class="ml-auto">
<EventMenu event={zapReceipt} />
</div>
</div>
{#if zapReceipt.content}

6
src/lib/modules/threads/ThreadCard.svelte

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<script lang="ts">
import ProfileBadge from '../../components/layout/ProfileBadge.svelte';
import EventMenu from '../../components/EventMenu.svelte';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
@ -204,7 +205,10 @@ @@ -204,7 +205,10 @@
<h3 class="text-lg font-semibold">
<a href="/thread/{thread.id}">{getTitle()}</a>
</h3>
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<div class="flex items-center gap-2">
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<EventMenu event={thread} showContentActions={true} />
</div>
</div>
<div class="mb-2 flex items-center gap-2">

57
src/lib/services/event-menu-store.ts

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
/**
* Global store for managing which EventMenu is currently open
* Ensures only one menu is open at a time
*/
type CloseCallback = () => void;
class EventMenuStore {
private openMenuId: string | null = null;
private closeCallback: CloseCallback | null = null;
/**
* Register a menu as open
* @param menuId Unique identifier for the menu (typically event.id)
* @param closeCallback Function to call to close this menu
*/
openMenu(menuId: string, closeCallback: CloseCallback): void {
// Close any previously open menu
if (this.openMenuId && this.openMenuId !== menuId && this.closeCallback) {
this.closeCallback();
}
this.openMenuId = menuId;
this.closeCallback = closeCallback;
}
/**
* Unregister a menu as open
* @param menuId Unique identifier for the menu
*/
closeMenu(menuId: string): void {
if (this.openMenuId === menuId) {
this.openMenuId = null;
this.closeCallback = null;
}
}
/**
* Check if a specific menu is open
*/
isOpen(menuId: string): boolean {
return this.openMenuId === menuId;
}
/**
* Close the currently open menu (if any)
*/
closeCurrentMenu(): void {
if (this.closeCallback) {
this.closeCallback();
}
this.openMenuId = null;
this.closeCallback = null;
}
}
export const eventMenuStore = new EventMenuStore();

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

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
/**
* User actions service - manages pinned, bookmarked, and highlighted events
* Stores in localStorage for persistence
*/
const STORAGE_KEY_PINNED = 'aitherboard_pinned_events';
const STORAGE_KEY_BOOKMARKED = 'aitherboard_bookmarked_events';
const STORAGE_KEY_HIGHLIGHTED = 'aitherboard_highlighted_events';
/**
* Get all pinned event IDs
*/
export function getPinnedEvents(): Set<string> {
if (typeof window === 'undefined') return new Set();
try {
const stored = localStorage.getItem(STORAGE_KEY_PINNED);
if (!stored) return new Set();
const ids = JSON.parse(stored) as string[];
return new Set(ids);
} catch {
return new Set();
}
}
/**
* Get all bookmarked event IDs
*/
export function getBookmarkedEvents(): Set<string> {
if (typeof window === 'undefined') return new Set();
try {
const stored = localStorage.getItem(STORAGE_KEY_BOOKMARKED);
if (!stored) return new Set();
const ids = JSON.parse(stored) as string[];
return new Set(ids);
} catch {
return new Set();
}
}
/**
* Get all highlighted event IDs
*/
export function getHighlightedEvents(): Set<string> {
if (typeof window === 'undefined') return new Set();
try {
const stored = localStorage.getItem(STORAGE_KEY_HIGHLIGHTED);
if (!stored) return new Set();
const ids = JSON.parse(stored) as string[];
return new Set(ids);
} catch {
return new Set();
}
}
/**
* Check if an event is pinned
*/
export function isPinned(eventId: string): boolean {
return getPinnedEvents().has(eventId);
}
/**
* Check if an event is bookmarked
*/
export function isBookmarked(eventId: string): boolean {
return getBookmarkedEvents().has(eventId);
}
/**
* Check if an event is highlighted
*/
export function isHighlighted(eventId: string): boolean {
return getHighlightedEvents().has(eventId);
}
/**
* Toggle pin status of an event
*/
export function togglePin(eventId: string): boolean {
const pinned = getPinnedEvents();
const isCurrentlyPinned = pinned.has(eventId);
if (isCurrentlyPinned) {
pinned.delete(eventId);
} else {
pinned.add(eventId);
}
try {
localStorage.setItem(STORAGE_KEY_PINNED, JSON.stringify(Array.from(pinned)));
return !isCurrentlyPinned;
} catch (error) {
console.error('Failed to save pinned events:', error);
return isCurrentlyPinned;
}
}
/**
* Toggle bookmark status of an event
*/
export function toggleBookmark(eventId: string): boolean {
const bookmarked = getBookmarkedEvents();
const isCurrentlyBookmarked = bookmarked.has(eventId);
if (isCurrentlyBookmarked) {
bookmarked.delete(eventId);
} else {
bookmarked.add(eventId);
}
try {
localStorage.setItem(STORAGE_KEY_BOOKMARKED, JSON.stringify(Array.from(bookmarked)));
return !isCurrentlyBookmarked;
} catch (error) {
console.error('Failed to save bookmarked events:', error);
return isCurrentlyBookmarked;
}
}
/**
* Toggle highlight status of an event
*/
export function toggleHighlight(eventId: string): boolean {
const highlighted = getHighlightedEvents();
const isCurrentlyHighlighted = highlighted.has(eventId);
if (isCurrentlyHighlighted) {
highlighted.delete(eventId);
} else {
highlighted.add(eventId);
}
try {
localStorage.setItem(STORAGE_KEY_HIGHLIGHTED, JSON.stringify(Array.from(highlighted)));
return !isCurrentlyHighlighted;
} catch (error) {
console.error('Failed to save highlighted events:', error);
return isCurrentlyHighlighted;
}
}

3
src/lib/types/kind-lookup.ts

@ -68,7 +68,8 @@ export const KIND_LOOKUP: Record<number, KindInfo> = { @@ -68,7 +68,8 @@ export const KIND_LOOKUP: Record<number, KindInfo> = {
10000: { number: 10000, description: 'Mute List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Pin lists
10001: { number: 10001, description: 'Pin List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
10001: { number: 10001, description: 'Pin List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
10009: { number: 10009, description: 'Bookmarks', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// Payment addresses
10133: { number: 10133, description: 'Payment Addresses', showInFeed: false, isReplaceable: false, isSecondaryKind: false },

Loading…
Cancel
Save