diff --git a/public/healthz.json b/public/healthz.json index d2f0770..ba89415 100644 --- a/public/healthz.json +++ b/public/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.1.1", - "buildTime": "2026-02-05T23:07:17.455Z", + "buildTime": "2026-02-06T07:27:23.473Z", "gitCommit": "unknown", - "timestamp": 1770332837455 + "timestamp": 1770362843473 } \ No newline at end of file diff --git a/src/app.css b/src/app.css index de520b5..3e2dbf5 100644 --- a/src/app.css +++ b/src/app.css @@ -286,8 +286,8 @@ main { } .emoji-grayscale { - filter: grayscale(100%); - opacity: 0.7; + filter: grayscale(100%) brightness(1.3); + opacity: 0.5; } /* Common button styles */ diff --git a/src/lib/components/EventMenu.svelte b/src/lib/components/EventMenu.svelte index 04b3c80..256c5a8 100644 --- a/src/lib/components/EventMenu.svelte +++ b/src/lib/components/EventMenu.svelte @@ -23,9 +23,10 @@ interface Props { event: NostrEvent; showContentActions?: boolean; // Show pin/bookmark/highlight for notes with content + onReply?: () => void; // Callback for reply action } - let { event, showContentActions = false }: Props = $props(); + let { event, showContentActions = false, onReply }: Props = $props(); let menuOpen = $state(false); let jsonModalOpen = $state(false); @@ -43,8 +44,7 @@ // 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 === KIND.SHORT_TEXT_NOTE || event.kind === KIND.DISCUSSION_THREAD); + // Note: Removed isContentNote check - all events should have the same menu (except profile pages/cards) // Check if user is logged in let isLoggedIn = $derived(sessionManager.isLoggedIn()); @@ -413,7 +413,13 @@ {/if} - {#if isLoggedIn && showContentActions && isContentNote} + {#if isLoggedIn && onReply} + + + {/if} + {#if isLoggedIn && showContentActions} + + + + + {/if} + + + showGifPicker = false} /> + showEmojiPicker = false} /> + + diff --git a/src/lib/components/layout/Header.svelte b/src/lib/components/layout/Header.svelte index 0e0e7fc..5e5ffce 100644 --- a/src/lib/components/layout/Header.svelte +++ b/src/lib/components/layout/Header.svelte @@ -63,8 +63,8 @@ /Relay /Topics /Repos - /Cache /Bookmarks + /Cache /Settings {#if isLoggedIn && currentPubkey} { e.preventDefault(); handleLogout(); }} class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Logout @@ -109,6 +109,9 @@ nav { min-width: 0; /* Allow flex items to shrink */ + position: sticky; + top: 0; + z-index: 100; } /* Responsive navigation links */ diff --git a/src/lib/components/layout/PubkeyFilter.svelte b/src/lib/components/layout/PubkeyFilter.svelte new file mode 100644 index 0000000..aac6dc5 --- /dev/null +++ b/src/lib/components/layout/PubkeyFilter.svelte @@ -0,0 +1,279 @@ + + +
+ + {#if resolving} + + {/if} +
+ + diff --git a/src/lib/components/layout/SearchBox.svelte b/src/lib/components/layout/SearchBox.svelte index ba80690..cbb53c5 100644 --- a/src/lib/components/layout/SearchBox.svelte +++ b/src/lib/components/layout/SearchBox.svelte @@ -64,6 +64,9 @@ return; } + // Ensure nostrClient is initialized + await nostrClient.initialize(); + searching = true; searchResults = []; showResults = true; @@ -76,7 +79,7 @@ if (decoded.type === 'event' && decoded.id) { // Search for specific event ID - let event = await getEvent(decoded.id); + let event: NostrEvent | undefined = await getEvent(decoded.id); if (!event) { // Not in cache, fetch from relays @@ -116,14 +119,21 @@ const kind11Events = await getEventsByKind(KIND.DISCUSSION_THREAD, 100); allCached.push(...kind11Events); - // Filter by search query + // Filter by search query - search title, summary, and content const queryLower = query.toLowerCase(); const matches = allCached.filter(event => { + // Search content const contentMatch = event.content.toLowerCase().includes(queryLower); - const tagMatch = event.tags.some(tag => - tag.some(val => val && val.toLowerCase().includes(queryLower)) - ); - return contentMatch || tagMatch; + + // Search title tag + const titleTag = event.tags.find(t => t[0] === 'title'); + const titleMatch = titleTag?.[1]?.toLowerCase().includes(queryLower) || false; + + // Search summary tag + const summaryTag = event.tags.find(t => t[0] === 'summary'); + const summaryMatch = summaryTag?.[1]?.toLowerCase().includes(queryLower) || false; + + return contentMatch || titleMatch || summaryMatch; }); // Sort by relevance (exact matches first, then by created_at) @@ -146,7 +156,8 @@ function handleSearchInput(e: Event) { const target = e.target as HTMLInputElement; - searchQuery = target.value; + const newValue = target.value; + searchQuery = newValue; // Clear existing timeout if (searchTimeout) { @@ -154,13 +165,16 @@ } // Debounce search - wait 300ms after user stops typing - if (searchQuery.trim()) { + if (newValue.trim()) { + // Show loading indicator immediately + searching = true; searchTimeout = setTimeout(() => { performSearch(); }, 300); } else { searchResults = []; showResults = false; + searching = false; } } diff --git a/src/lib/components/layout/UnifiedSearch.svelte b/src/lib/components/layout/UnifiedSearch.svelte new file mode 100644 index 0000000..87d9bda --- /dev/null +++ b/src/lib/components/layout/UnifiedSearch.svelte @@ -0,0 +1,938 @@ + + +
+
+ {#if showKindFilter} + + {/if} + + {#if searching || resolving} + + {/if} +
+ + {#if mode === 'search' && !hideDropdownResults && showResults && searchResults.length > 0} +
+ {#each searchResults as { event, matchType }} + + {/each} +
+ {:else if mode === 'search' && !hideDropdownResults && showResults && !searching && searchQuery.trim()} +
+
No results found
+
+ {/if} +
+ + diff --git a/src/lib/components/profile/BookmarksPanel.svelte b/src/lib/components/profile/BookmarksPanel.svelte index 7cf76d8..84398e2 100644 --- a/src/lib/components/profile/BookmarksPanel.svelte +++ b/src/lib/components/profile/BookmarksPanel.svelte @@ -4,10 +4,10 @@ import { relayManager } from '../../services/nostr/relay-manager.js'; import { sessionManager } from '../../services/auth/session-manager.js'; import FeedPost from '../../modules/feed/FeedPost.svelte'; - import ThreadDrawer from '../../modules/feed/ThreadDrawer.svelte'; import { onMount } from 'svelte'; import type { NostrEvent } from '../../types/nostr.js'; import { KIND } from '../../types/kind-lookup.js'; + import { goto } from '$app/navigation'; interface Props { isOpen: boolean; @@ -18,17 +18,9 @@ let bookmarkedEvents = $state([]); let loading = $state(true); - let drawerOpen = $state(false); - let drawerEvent = $state(null); - function openDrawer(event: NostrEvent) { - drawerEvent = event; - drawerOpen = true; - } - - function closeDrawer() { - drawerOpen = false; - drawerEvent = null; + function navigateToEvent(event: NostrEvent) { + goto(`/event/${event.id}`); } $effect(() => { @@ -115,7 +107,7 @@ {:else}
{#each bookmarkedEvents as event (event.id)} - + {/each}
{/if} @@ -124,8 +116,6 @@ {/if} - - diff --git a/src/lib/modules/comments/CommentForm.svelte b/src/lib/modules/comments/CommentForm.svelte index 045c030..8423e36 100644 --- a/src/lib/modules/comments/CommentForm.svelte +++ b/src/lib/modules/comments/CommentForm.svelte @@ -7,16 +7,13 @@ import { fetchRelayLists } from '../../services/user-data.js'; import { getDraft, saveDraft, deleteDraft } from '../../services/cache/draft-store.js'; import PublicationStatusModal from '../../components/modals/PublicationStatusModal.svelte'; - import GifPicker from '../../components/content/GifPicker.svelte'; - import EmojiPicker from '../../components/content/EmojiPicker.svelte'; import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; import MediaAttachments from '../../components/content/MediaAttachments.svelte'; - import { insertTextAtCursor } from '../../services/text-utils.js'; + import RichTextEditor from '../../components/content/RichTextEditor.svelte'; import type { NostrEvent } from '../../types/nostr.js'; import { KIND } from '../../types/kind-lookup.js'; import { shouldIncludeClientTag } from '../../services/client-tag-preference.js'; import { cacheEvent } from '../../services/cache/event-cache.js'; - import MentionsAutocomplete from '../../components/content/MentionsAutocomplete.svelte'; import { extractMentions, getMentionPubkeys } from '../../services/mentions.js'; interface Props { @@ -75,13 +72,9 @@ }); let showStatusModal = $state(false); let publicationResults: { success: string[]; failed: Array<{ relay: string; error: string }> } | null = $state(null); - let showGifPicker = $state(false); - let showEmojiPicker = $state(false); let showJsonModal = $state(false); let showPreviewModal = $state(false); - let textareaRef: HTMLTextAreaElement | null = $state(null); - let fileInputRef: HTMLInputElement | null = $state(null); - let uploading = $state(false); + let richTextEditorRef: { clearUploadedFiles: () => void; getUploadedFiles: () => Array<{ url: string; imetaTag: string[] }> } | null = $state(null); let uploadedFiles: Array<{ url: string; imetaTag: string[] }> = $state([]); let eventJson = $state('{}'); const isLoggedIn = $derived(sessionManager.isLoggedIn()); @@ -274,22 +267,16 @@ if (confirm('Are you sure you want to clear the comment? This will delete all unsaved content.')) { content = ''; uploadedFiles = []; + if (richTextEditorRef) { + richTextEditorRef.clearUploadedFiles(); + } // Clear draft from IndexedDB await deleteDraft(DRAFT_ID); } } - function handleGifSelect(gifUrl: string) { - if (!textareaRef) return; - // Insert GIF URL as plain text - insertTextAtCursor(textareaRef, gifUrl); - showGifPicker = false; - } - - function handleEmojiSelect(emoji: string) { - if (!textareaRef) return; - insertTextAtCursor(textareaRef, emoji); - showEmojiPicker = false; + function handleFilesUploaded(files: Array<{ url: string; imetaTag: string[] }>) { + uploadedFiles = files; } async function getEventJson(): Promise { @@ -360,104 +347,20 @@ } - async function handleFileUpload(event: Event) { - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - if (!file) return; - - // Check file type - const isImage = file.type.startsWith('image/'); - const isVideo = file.type.startsWith('video/'); - const isAudio = file.type.startsWith('audio/'); - - if (!isImage && !isVideo && !isAudio) { - alert('Please select an image, video, or audio file'); - return; - } - - if (!sessionManager.isLoggedIn()) { - alert('Please log in to upload files'); - return; - } - - uploading = true; - try { - // Upload file to media server - const uploadResult = await uploadFileToServer(file, 'CommentForm'); - console.log(`[CommentForm] Uploaded ${file.name} to ${uploadResult.url}`, { tags: uploadResult.tags }); - - // Build imeta tag from upload response (NIP-92 format) - const imetaTag = buildImetaTag(file, uploadResult); - console.log(`[CommentForm] Built imeta tag for ${file.name}:`, imetaTag); - - // Store file with imeta tag - uploadedFiles.push({ - url: uploadResult.url, - imetaTag - }); - - // Insert file URL into textarea (plain URL for all file types) - if (textareaRef) { - insertTextAtCursor(textareaRef, `${uploadResult.url}\n`); - } - } catch (error) { - console.error('[CommentForm] File upload failed:', error); - const errorMessage = error instanceof Error ? error.message : String(error); - alert(`Failed to upload file: ${errorMessage}`); - } finally { - uploading = false; - // Reset file input - if (fileInputRef) { - fileInputRef.value = ''; - } - } - } {#if isLoggedIn}
-
- - - {#if textareaRef} - - {/if} - - {#if showGifButton} -
- - -
- {/if} -
+
@@ -491,22 +394,6 @@ > Clear - -
{#if onCancel} @@ -529,11 +416,6 @@
- - {#if showGifButton} - showGifPicker = false} /> - showEmojiPicker = false} /> - {/if} {#if showJsonModal} @@ -658,65 +540,6 @@ } } - .textarea-wrapper { - position: relative; - } - - textarea { - resize: vertical; - min-height: 100px; - } - - /* Add padding to bottom when buttons are visible to prevent text overlap */ - textarea.has-buttons { - padding-bottom: 2.5rem; - } - - textarea:focus { - outline: none; - border-color: var(--fog-accent, #64748b); - } - - .textarea-buttons { - position: absolute; - bottom: 0.5rem; - left: 0.5rem; - display: flex; - gap: 0.25rem; - z-index: 10; - } - - .toolbar-button { - padding: 0.25rem 0.5rem; - font-size: 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; - transition: all 0.2s; - } - - .toolbar-button:hover:not(:disabled) { - background: var(--fog-highlight, #f3f4f6); - border-color: var(--fog-accent, #64748b); - } - - .toolbar-button:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - :global(.dark) .toolbar-button { - background: var(--fog-dark-post, #1f2937); - border-color: var(--fog-dark-border, #374151); - color: var(--fog-dark-text, #f9fafb); - } - - :global(.dark) .toolbar-button:hover:not(:disabled) { - background: var(--fog-dark-highlight, #374151); - border-color: var(--fog-dark-accent, #64748b); - } /* Modal styles */ .modal-overlay { @@ -862,15 +685,6 @@ border-color: #64748b; } - label[for="comment-file-upload"], - .upload-label { - user-select: none; - } - - .upload-label { - filter: grayscale(100%); - } - .comment-form-actions { flex-wrap: wrap; gap: 0.5rem; @@ -901,9 +715,5 @@ flex: 1; min-width: 0; } - - textarea { - font-size: 16px; /* Prevent zoom on iOS */ - } } diff --git a/src/lib/modules/comments/CommentThread.svelte b/src/lib/modules/comments/CommentThread.svelte index f53946e..3d53f67 100644 --- a/src/lib/modules/comments/CommentThread.svelte +++ b/src/lib/modules/comments/CommentThread.svelte @@ -15,9 +15,10 @@ event?: NostrEvent; // The root event itself (optional, used to determine reply types) onCommentsLoaded?: (eventIds: string[]) => void; // Callback when comments are loaded preloadedReactions?: Map; // Pre-loaded reactions by event ID + hideCommentForm?: boolean; // If true, don't show the comment form at the bottom } - let { threadId, event, onCommentsLoaded, preloadedReactions }: Props = $props(); + let { threadId, event, onCommentsLoaded, preloadedReactions, hideCommentForm = false }: Props = $props(); let comments = $state([]); // kind 1111 let kind1Replies = $state([]); // kind 1 replies (should only be for kind 1 events, but some apps use them for everything) @@ -782,6 +783,9 @@ parentEvent={parent} onReply={handleReply} rootEventKind={rootKind ?? undefined} + threadId={threadId} + rootEvent={event} + onCommentPublished={handleCommentPublished} /> {:else if item.type === 'reply'} @@ -813,24 +817,26 @@
{/if} - {#if replyingTo} -
- (replyingTo = null)} - /> -
- {:else} -
- -
+ {#if !hideCommentForm} + {#if replyingTo} +
+ (replyingTo = null)} + /> +
+ {:else} +
+ +
+ {/if} {/if}
diff --git a/src/lib/modules/discussions/DiscussionCard.svelte b/src/lib/modules/discussions/DiscussionCard.svelte index 490867a..d9e5832 100644 --- a/src/lib/modules/discussions/DiscussionCard.svelte +++ b/src/lib/modules/discussions/DiscussionCard.svelte @@ -5,6 +5,9 @@ import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; import MediaAttachments from '../../components/content/MediaAttachments.svelte'; import MetadataCard from '../../components/content/MetadataCard.svelte'; + import EventMenu from '../../components/EventMenu.svelte'; + import CommentForm from '../comments/CommentForm.svelte'; + import { sessionManager } from '../../services/auth/session-manager.js'; import { nostrClient } from '../../services/nostr/nostr-client.js'; import { relayManager } from '../../services/nostr/relay-manager.js'; import { onMount } from 'svelte'; @@ -37,6 +40,8 @@ let contentElement: HTMLElement | null = $state(null); let needsExpansion = $state(false); let lastStatsLoadEventId = $state(null); + let showReplyForm = $state(false); + let isLoggedIn = $derived(sessionManager.isLoggedIn()); onMount(async () => { await loadStats(); @@ -211,8 +216,20 @@ {getTitle()}
- {getRelativeTime()} - + {getRelativeTime()} +
e.stopPropagation()} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + } + }} + role="button" + tabindex="0" + > + showReplyForm = !showReplyForm} /> +
@@ -241,13 +258,25 @@ {:else}
-
-

+
+

{getTitle()}

-
- {getRelativeTime()} - +
+ {getRelativeTime()} +
e.stopPropagation()} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + } + }} + role="button" + tabindex="0" + > + showReplyForm = !showReplyForm} /> +
@@ -289,7 +318,7 @@ {/if} -
+
{#if fullView} @@ -334,6 +363,21 @@
+{#if isLoggedIn && showReplyForm && fullView} +
+ { + showReplyForm = false; + }} + onCancel={() => { + showReplyForm = false; + }} + /> +
+{/if} + diff --git a/src/lib/modules/discussions/DiscussionList.svelte b/src/lib/modules/discussions/DiscussionList.svelte index a431507..1050849 100644 --- a/src/lib/modules/discussions/DiscussionList.svelte +++ b/src/lib/modules/discussions/DiscussionList.svelte @@ -3,11 +3,43 @@ import { relayManager } from '../../services/nostr/relay-manager.js'; import { config } from '../../services/nostr/config.js'; import DiscussionCard from './DiscussionCard.svelte'; - import ThreadDrawer from '../feed/ThreadDrawer.svelte'; import type { NostrEvent } from '../../types/nostr.js'; import { onMount } from 'svelte'; import { KIND } from '../../types/kind-lookup.js'; import { getRecentCachedEvents } from '../../services/cache/event-cache.js'; + import { nip19 } from 'nostr-tools'; + import { goto } from '$app/navigation'; + + interface Props { + filterResult?: { type: 'event' | 'pubkey' | 'text' | null; value: string | null }; + } + + let { filterResult = { type: null, value: null } }: Props = $props(); + + // Resolved pubkey from filter (handled by parent component's PubkeyFilter) + // For now, we'll do basic normalization here since we don't have access to the filter component + // The parent component should resolve NIP-05 before passing it here + function normalizeToHex(input: string): string | null { + const trimmed = input.trim(); + if (!trimmed) return null; + + if (/^[a-fA-F0-9]{64}$/.test(trimmed)) { + return trimmed.toLowerCase(); + } + + try { + const decoded = nip19.decode(trimmed); + if (decoded.type === 'npub') { + return decoded.data as string; + } else if (decoded.type === 'nprofile') { + return (decoded.data as any).pubkey; + } + } catch { + // Not a valid bech32 + } + + return null; + } // Data maps - threads and stats for sorting only (DiscussionCard loads its own stats for display) let threadsMap = $state>(new Map()); // threadId -> thread @@ -22,9 +54,6 @@ let showOlder = $state(false); let selectedTopic = $state(null); // null = All, undefined = General, string = specific topic - // Thread drawer state - let drawerOpen = $state(false); - let selectedEvent = $state(null); // Computed: get sorted and filtered threads from maps let threads = $derived.by(() => { @@ -73,11 +102,16 @@ // Skip if we haven't set initial values yet (onMount hasn't run) if (prevSortBy === null) return; + // Read showOlder to ensure it's tracked by the effect + const currentShowOlder = showOlder; + const currentSortBy = sortBy; + const currentSelectedTopic = selectedTopic; + // Check if any filter parameter actually changed - if (sortBy !== prevSortBy || showOlder !== prevShowOlder || selectedTopic !== prevSelectedTopic) { - prevSortBy = sortBy; - prevShowOlder = showOlder; - prevSelectedTopic = selectedTopic; + if (currentSortBy !== prevSortBy || currentShowOlder !== prevShowOlder || currentSelectedTopic !== prevSelectedTopic) { + prevSortBy = currentSortBy; + prevShowOlder = currentShowOlder; + prevSelectedTopic = currentSelectedTopic; // Only reload if not already loading if (!isLoading) { @@ -387,6 +421,7 @@ voteCountsReady = false; } finally { loading = false; + isLoading = false; } } @@ -472,13 +507,47 @@ return events.filter((t) => t.created_at >= cutoffTime); } - // Get filtered threads (by age and topic) - reactive derived value + // Get filtered threads (by age, topic, and filter result) - reactive derived value let filteredThreads = $derived.by(() => { let filtered = threads; // Filter by age first filtered = filterByAge(filtered); + // Apply filter based on filterResult type + if (filterResult.type === 'event' && filterResult.value) { + // Filter by specific event ID + filtered = filtered.filter(t => t.id === filterResult.value); + } else if (filterResult.type === 'pubkey' && filterResult.value) { + // Filter by pubkey (should already be normalized hex) + const normalizedPubkey = filterResult.value.toLowerCase(); + if (/^[a-f0-9]{64}$/i.test(normalizedPubkey)) { + filtered = filtered.filter(t => t.pubkey.toLowerCase() === normalizedPubkey); + } + } else if (filterResult.type === 'text' && filterResult.value) { + // Filter by text search (pubkey, p, q, and content fields) + const queryLower = filterResult.value.toLowerCase(); + filtered = filtered.filter(event => { + // Search pubkey + const pubkeyMatch = event.pubkey.toLowerCase().includes(queryLower); + + // Search p tags + const pTagMatch = event.tags.some(tag => + tag[0] === 'p' && tag[1]?.toLowerCase().includes(queryLower) + ); + + // Search q tags + const qTagMatch = event.tags.some(tag => + tag[0] === 'q' && tag.some((val, idx) => idx > 0 && val?.toLowerCase().includes(queryLower)) + ); + + // Search content + const contentMatch = event.content.toLowerCase().includes(queryLower); + + return pubkeyMatch || pTagMatch || qTagMatch || contentMatch; + }); + } + // Then filter by topic // selectedTopic === null means "All" - show all threads if (selectedTopic === null) { @@ -526,28 +595,22 @@ return result; } - function openThreadDrawer(event: NostrEvent, e?: MouseEvent) { - // Don't open drawer if clicking on interactive elements + function navigateToEvent(event: NostrEvent, e?: MouseEvent) { + // Don't navigate if clicking on interactive elements if (e) { const target = e.target as HTMLElement; if (target.closest('button') || target.closest('a') || target.closest('[role="button"]')) { return; } } - selectedEvent = event; - drawerOpen = true; - } - - function closeThreadDrawer() { - drawerOpen = false; - selectedEvent = null; + goto(`/event/${event.id}`); } onMount(() => { // Listen for custom event from EmbeddedEvent components const handleOpenEvent = (e: CustomEvent) => { if (e.detail?.event) { - openThreadDrawer(e.detail.event); + navigateToEvent(e.detail.event); } }; @@ -575,12 +638,6 @@ { - // If showing older threads, reload to fetch them - if (showOlder) { - loadAllData(); - } - }} /> Show older posts (than 30 days) @@ -621,13 +678,13 @@
openThreadDrawer(thread, e)} + onclick={(e) => navigateToEvent(thread, e)} role="button" tabindex="0" onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - openThreadDrawer(thread); + navigateToEvent(thread); } }} > @@ -647,12 +704,6 @@ {/if}
- - diff --git a/src/lib/modules/feed/HighlightCard.svelte b/src/lib/modules/feed/HighlightCard.svelte index a3c55ff..a49e2e0 100644 --- a/src/lib/modules/feed/HighlightCard.svelte +++ b/src/lib/modules/feed/HighlightCard.svelte @@ -323,7 +323,7 @@ {#if isLoggedIn && bookmarked} 🔖 {/if} - + {}} />
diff --git a/src/lib/modules/feed/Reply.svelte b/src/lib/modules/feed/Reply.svelte index 9873e6b..851815b 100644 --- a/src/lib/modules/feed/Reply.svelte +++ b/src/lib/modules/feed/Reply.svelte @@ -92,7 +92,7 @@ via {getClientName()} {/if}
- + {}} />

diff --git a/src/lib/modules/feed/ThreadDrawer.svelte b/src/lib/modules/feed/ThreadDrawer.svelte deleted file mode 100644 index 268015b..0000000 --- a/src/lib/modules/feed/ThreadDrawer.svelte +++ /dev/null @@ -1,561 +0,0 @@ - - -{#if isOpen && opEvent} -
- -{/if} - - diff --git a/src/lib/modules/feed/ZapReceiptReply.svelte b/src/lib/modules/feed/ZapReceiptReply.svelte index 2050dc1..47cb734 100644 --- a/src/lib/modules/feed/ZapReceiptReply.svelte +++ b/src/lib/modules/feed/ZapReceiptReply.svelte @@ -100,7 +100,7 @@ {getAmount().toLocaleString()} sats {getRelativeTime()}
- +
diff --git a/src/lib/types/kind-lookup.ts b/src/lib/types/kind-lookup.ts index 843d952..940f7eb 100644 --- a/src/lib/types/kind-lookup.ts +++ b/src/lib/types/kind-lookup.ts @@ -108,7 +108,7 @@ export const KIND_LOOKUP: Record = { // Articles [KIND.LONG_FORM_NOTE]: { number: KIND.LONG_FORM_NOTE, description: 'Long-form Note', showInFeed: true, isSecondaryKind: false }, - [KIND.HIGHLIGHTED_ARTICLE]: { number: KIND.HIGHLIGHTED_ARTICLE, description: 'Highlighted Article', showInFeed: true, isSecondaryKind: false }, + [KIND.HIGHLIGHTED_ARTICLE]: { number: KIND.HIGHLIGHTED_ARTICLE, description: 'Highlighted Article', showInFeed: false, isSecondaryKind: false }, // Threads and comments [KIND.DISCUSSION_THREAD]: { number: KIND.DISCUSSION_THREAD, description: 'Discussion Thread', showInFeed: false, isSecondaryKind: false }, // Only shown on /discussions page diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 590ca5f..37dbb6b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -12,8 +12,27 @@ let { children }: Props = $props(); - // Restore session immediately if in browser (before onMount) + // Initialize theme and preferences from localStorage immediately (before any components render) if (browser) { + // Initialize theme + const storedTheme = localStorage.getItem('theme'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const shouldBeDark = storedTheme === 'dark' || (!storedTheme && prefersDark); + if (shouldBeDark) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + + // Initialize other preferences + const textSize = localStorage.getItem('textSize') || 'medium'; + const lineSpacing = localStorage.getItem('lineSpacing') || 'normal'; + const contentWidth = localStorage.getItem('contentWidth') || 'medium'; + + document.documentElement.setAttribute('data-text-size', textSize); + document.documentElement.setAttribute('data-line-spacing', lineSpacing); + document.documentElement.setAttribute('data-content-width', contentWidth); + // Try to restore session synchronously if possible // This ensures session is restored before any components render (async () => { diff --git a/src/routes/bookmarks/+page.svelte b/src/routes/bookmarks/+page.svelte index 588ba8d..b0db898 100644 --- a/src/routes/bookmarks/+page.svelte +++ b/src/routes/bookmarks/+page.svelte @@ -1,40 +1,82 @@ @@ -120,29 +328,62 @@ {#if loading}
-

Loading bookmarks...

+

Loading bookmarks and highlights...

{:else if error}

{error}

- {:else if allEvents.length === 0} + {:else if allItems.length === 0}
-

No bookmarks found.

+

No bookmarks or highlights found.

{:else} +
+
+ + +
+
+ +
+
+

- Showing {paginatedEvents.length} of {allEvents.length} bookmarks - {#if allEvents.length >= maxTotalBookmarks} - (limited to {maxTotalBookmarks}) + Showing {paginatedItems.length} of {filteredItems.length} items + {#if allItems.length >= maxTotalItems} + (limited to {maxTotalItems}) + {/if} + {#if filterResult.value} + (filtered) {/if}

- {#each paginatedEvents as event (event.id)} - + {#each paginatedItems as item (item.event.id)} +
+
+ + {item.type === 'bookmark' ? '🔖' : '✨'} + +
+ +
{/each}
@@ -177,6 +418,38 @@ {/if} + + {#if totalPages > 1} + + {/if} {/if} @@ -260,4 +533,88 @@ min-width: 100px; text-align: center; } + + .filters-section { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; + } + + .type-filter-section { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .type-filter-label { + font-size: 0.875rem; + color: var(--fog-text, #1f2937); + font-weight: 500; + } + + :global(.dark) .type-filter-label { + color: var(--fog-dark-text, #f9fafb); + } + + .type-filter-select { + padding: 0.5rem 0.75rem; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.375rem; + background: var(--fog-post, #ffffff); + color: var(--fog-text, #1f2937); + font-size: 0.875rem; + cursor: pointer; + min-width: 200px; + font-family: inherit; + } + + .type-filter-select:focus { + outline: none; + border-color: var(--fog-accent, #64748b); + box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1); + } + + :global(.dark) .type-filter-select { + border-color: var(--fog-dark-border, #374151); + background: var(--fog-dark-post, #1f2937); + color: var(--fog-dark-text, #f9fafb); + } + + :global(.dark) .type-filter-select:focus { + border-color: var(--fog-dark-accent, #94a3b8); + box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1); + } + + .search-filter-section { + width: 100%; + } + + .bookmark-item-wrapper { + position: relative; + } + + .bookmark-indicator-wrapper { + position: absolute; + top: 0.5rem; + left: 0.5rem; + z-index: 10; + pointer-events: none; + } + + .bookmark-item-wrapper :global(.Feed-post) { + padding-left: 2.5rem; /* Make room for the icon */ + } + + .bookmark-emoji { + display: inline-block; + font-size: 1.25rem; + line-height: 1; + filter: grayscale(100%); + transition: filter 0.2s; + } + + .bookmark-emoji:not(.grayscale) { + filter: grayscale(0%); + } diff --git a/src/routes/discussions/+page.svelte b/src/routes/discussions/+page.svelte index d26af16..4bbec71 100644 --- a/src/routes/discussions/+page.svelte +++ b/src/routes/discussions/+page.svelte @@ -1,10 +1,16 @@
@@ -119,42 +45,86 @@

/Find

- -
- -
- -
-

Find User

-

Enter a user ID (NIP-05, hex pubkey, npub, or nprofile)

+

Search Events

+

Search for events by ID, pubkey, NIP-05, or content. Use the kind filter to narrow results.

-
- { - if (e.key === 'Enter') { - findUser(); - } - }} - disabled={searching} - /> - +
+
+ +
+ +
+
+ + +
+ + +
- - {#if error} -
{error}
- {/if}
+ + {#if searchResults.events.length > 0 || searchResults.profiles.length > 0} +
+

Search Results

+ + {#if searchResults.profiles.length > 0} +
+

Profiles

+
+ {#each searchResults.profiles as pubkey} + + + + {/each} +
+
+ {/if} + + {#if searchResults.events.length > 0} +
+

Events ({searchResults.events.length})

+
+ {#each searchResults.events as event} + + + + {/each} +
+
+ {/if} +
+ {:else if !searching && (unifiedSearchComponent?.getFilterResult()?.value)} +
+
No results found
+
+ {/if}
@@ -205,69 +175,245 @@ color: var(--fog-dark-text-light, #9ca3af); } - .input-group { + .search-container { display: flex; - gap: 0.5rem; + flex-direction: column; + gap: 1rem; + } + + .search-bar-wrapper { + width: 100%; + } + + .filter-and-button-wrapper { + display: flex; + flex-direction: column; + gap: 1rem; + } + + @media (min-width: 640px) { + .filter-and-button-wrapper { + flex-direction: row; + align-items: flex-end; + } } - .user-input { + .kind-filter-wrapper { + display: flex; + flex-direction: column; + gap: 0.5rem; flex: 1; + } + + @media (min-width: 640px) { + .kind-filter-wrapper { + flex-direction: row; + align-items: center; + flex: 1; + } + } + + .kind-filter-label { + font-size: 0.875rem; + color: var(--fog-text, #1f2937); + font-weight: 500; + white-space: nowrap; + } + + :global(.dark) .kind-filter-label { + color: var(--fog-dark-text, #f9fafb); + } + + .kind-filter-select { padding: 0.75rem; border: 1px solid var(--fog-border, #e5e7eb); - border-radius: 0.25rem; + border-radius: 0.375rem; background: var(--fog-post, #ffffff); - color: var(--fog-text, #475569); + color: var(--fog-text, #1f2937); font-size: 0.875rem; - font-family: monospace; + cursor: pointer; + width: 100%; + font-family: inherit; + } + + @media (min-width: 640px) { + .kind-filter-select { + width: auto; + min-width: 200px; + } + } + + .kind-filter-select:focus { + outline: none; + border-color: var(--fog-accent, #64748b); + box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1); } - :global(.dark) .user-input { + :global(.dark) .kind-filter-select { border-color: var(--fog-dark-border, #374151); background: var(--fog-dark-post, #1f2937); - color: var(--fog-dark-text, #cbd5e1); + color: var(--fog-dark-text, #f9fafb); } - .user-input:disabled { - opacity: 0.6; - cursor: not-allowed; + :global(.dark) .kind-filter-select:focus { + border-color: var(--fog-dark-accent, #94a3b8); + box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1); } - .find-button { + .search-button { padding: 0.75rem 1.5rem; background: var(--fog-accent, #64748b); color: var(--fog-text, #f1f5f9); border: none; - border-radius: 0.25rem; + border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem; font-weight: 500; - font-family: monospace; + font-family: inherit; + white-space: nowrap; + transition: all 0.2s; + min-width: 100px; + } + + @media (min-width: 640px) { + .search-button { + min-width: auto; + } } - :global(.dark) .find-button { + :global(.dark) .search-button { background: var(--fog-dark-accent, #94a3b8); + color: var(--fog-dark-text, #1f2937); } - .find-button:hover:not(:disabled) { + .search-button:hover:not(:disabled) { opacity: 0.9; + transform: translateY(-1px); + } + + .search-button:active:not(:disabled) { + transform: translateY(0); } - .find-button:disabled { + .search-button:disabled { opacity: 0.6; cursor: not-allowed; } - .error-message { - margin-top: 1rem; - padding: 0.75rem; - background: var(--fog-danger-light, #fee2e2); - color: var(--fog-danger, #dc2626); - border-radius: 0.25rem; + + .results-section { + margin-top: 2rem; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.5rem; + padding: 2rem; + background: var(--fog-post, #ffffff); + } + + :global(.dark) .results-section { + border-color: var(--fog-dark-border, #374151); + background: var(--fog-dark-post, #1f2937); + } + + .results-section h2 { + margin: 0 0 1.5rem 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--fog-text, #475569); + } + + :global(.dark) .results-section h2 { + color: var(--fog-dark-text, #cbd5e1); + } + + .results-group { + margin-bottom: 2rem; + } + + .results-group:last-child { + margin-bottom: 0; + } + + .results-group h3 { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 500; + color: var(--fog-text-light, #6b7280); + } + + :global(.dark) .results-group h3 { + color: var(--fog-dark-text-light, #9ca3af); + } + + .profile-results { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .profile-result-card { + padding: 1rem; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.375rem; + background: var(--fog-post, #ffffff); + text-decoration: none; + transition: all 0.2s; + } + + :global(.dark) .profile-result-card { + border-color: var(--fog-dark-border, #374151); + background: var(--fog-dark-post, #1f2937); + } + + .profile-result-card:hover { + border-color: var(--fog-accent, #64748b); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + :global(.dark) .profile-result-card:hover { + border-color: var(--fog-dark-accent, #94a3b8); + } + + .event-results { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .event-result-card { + display: block; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.375rem; + background: var(--fog-post, #ffffff); + overflow: hidden; + transition: all 0.2s; + text-decoration: none; + color: inherit; + } + + :global(.dark) .event-result-card { + border-color: var(--fog-dark-border, #374151); + background: var(--fog-dark-post, #1f2937); + } + + .event-result-card:hover { + border-color: var(--fog-accent, #64748b); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + :global(.dark) .event-result-card:hover { + border-color: var(--fog-dark-accent, #94a3b8); + } + + .no-results { + padding: 2rem; + text-align: center; + color: var(--fog-text-light, #6b7280); font-size: 0.875rem; } - :global(.dark) .error-message { - background: var(--fog-dark-danger-light, #7f1d1d); - color: var(--fog-dark-danger, #ef4444); + :global(.dark) .no-results { + color: var(--fog-dark-text-light, #9ca3af); } diff --git a/src/routes/replaceable/[d_tag]/+page.svelte b/src/routes/replaceable/[d_tag]/+page.svelte index 2591eb9..ef48d02 100644 --- a/src/routes/replaceable/[d_tag]/+page.svelte +++ b/src/routes/replaceable/[d_tag]/+page.svelte @@ -1,19 +1,17 @@ @@ -117,11 +109,11 @@ {#each events as event (event.id)}
openInDrawer(event)} + onclick={() => navigateToEvent(event)} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - openInDrawer(event); + navigateToEvent(event); } }} role="button" @@ -135,10 +127,6 @@
-{#if drawerOpen && drawerEvent} - -{/if} -