From 7592c5c327af804802246b88f12b1d6ffddb6e50 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 13:58:41 +0200 Subject: [PATCH 01/43] feat(bookmarks): add sourceKind to IndividualBookmark for grouping --- src/types/bookmarks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/bookmarks.ts b/src/types/bookmarks.ts index 24cb3d62..7cb7c8ea 100644 --- a/src/types/bookmarks.ts +++ b/src/types/bookmarks.ts @@ -42,6 +42,8 @@ export interface IndividualBookmark { encryptedContent?: string // When the item was added to the bookmark list (synthetic, for sorting) added_at?: number + // The kind of the source list/set that produced this bookmark (e.g., 10003, 30003, 30001, or 39701 for web) + sourceKind?: number } export interface ActiveAccount { From 29e351ba78606244471ce647534a3c4a48cbac10 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 13:58:48 +0200 Subject: [PATCH 02/43] feat(bookmarks): tag sourceKind in collection for web and list/set items --- src/services/bookmarkProcessing.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/services/bookmarkProcessing.ts b/src/services/bookmarkProcessing.ts index 54cb87f1..645391c4 100644 --- a/src/services/bookmarkProcessing.ts +++ b/src/services/bookmarkProcessing.ts @@ -45,13 +45,19 @@ export async function collectBookmarksFromEvents( parsedContent: undefined, type: 'web' as const, isPrivate: false, - added_at: evt.created_at || Math.floor(Date.now() / 1000) + added_at: evt.created_at || Math.floor(Date.now() / 1000), + sourceKind: 39701 }) continue } const pub = Helpers.getPublicBookmarks(evt) - publicItemsAll.push(...processApplesauceBookmarks(pub, activeAccount, false)) + publicItemsAll.push( + ...processApplesauceBookmarks(pub, activeAccount, false).map(i => ({ + ...i, + sourceKind: evt.kind + })) + ) try { if (Helpers.hasHiddenTags(evt) && !Helpers.isHiddenTagsUnlocked(evt) && signerCandidate) { @@ -94,7 +100,12 @@ export async function collectBookmarksFromEvents( try { const hiddenTags = JSON.parse(decryptedContent) as string[][] const manualPrivate = Helpers.parseBookmarkTags(hiddenTags) - privateItemsAll.push(...processApplesauceBookmarks(manualPrivate, activeAccount, true)) + privateItemsAll.push( + ...processApplesauceBookmarks(manualPrivate, activeAccount, true).map(i => ({ + ...i, + sourceKind: evt.kind + })) + ) Reflect.set(evt, BookmarkHiddenSymbol, manualPrivate) Reflect.set(evt, 'EncryptedContentSymbol', decryptedContent) // Don't set latestContent to decrypted JSON - it's not user-facing content @@ -106,7 +117,12 @@ export async function collectBookmarksFromEvents( const priv = Helpers.getHiddenBookmarks(evt) if (priv) { - privateItemsAll.push(...processApplesauceBookmarks(priv, activeAccount, true)) + privateItemsAll.push( + ...processApplesauceBookmarks(priv, activeAccount, true).map(i => ({ + ...i, + sourceKind: evt.kind + })) + ) } } catch { // ignore individual event failures From 1c0790bfb6deea8934f80e186e40da1cd76cf86e Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 13:58:56 +0200 Subject: [PATCH 03/43] feat(bookmarks): add grouping and sorting helpers for sections --- src/utils/bookmarkUtils.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/utils/bookmarkUtils.tsx b/src/utils/bookmarkUtils.tsx index 14ba5f39..501ed2f4 100644 --- a/src/utils/bookmarkUtils.tsx +++ b/src/utils/bookmarkUtils.tsx @@ -1,6 +1,6 @@ import React from 'react' import { formatDistanceToNow, differenceInSeconds, differenceInMinutes, differenceInHours, differenceInDays, differenceInMonths, differenceInYears } from 'date-fns' -import { ParsedContent, ParsedNode } from '../types/bookmarks' +import { ParsedContent, ParsedNode, IndividualBookmark } from '../types/bookmarks' import ResolvedMention from '../components/ResolvedMention' // Note: ContentWithResolvedProfiles is imported by components directly to keep this file component-only for fast refresh @@ -82,3 +82,20 @@ export const renderParsedContent = (parsedContent: ParsedContent) => { ) } + +// Sorting and grouping for bookmarks +export const sortIndividualBookmarks = (items: IndividualBookmark[]) => { + return items + .slice() + .sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0))) +} + +export function groupIndividualBookmarks(items: IndividualBookmark[]) { + const sorted = sortIndividualBookmarks(items) + const amethyst = sorted.filter(i => i.sourceKind === 30001) + const web = sorted.filter(i => i.kind === 39701 || i.type === 'web') + const isIn = (list: IndividualBookmark[], x: IndividualBookmark) => list.some(i => i.id === x.id) + const privateItems = sorted.filter(i => i.isPrivate && !isIn(amethyst, i) && !isIn(web, i)) + const publicItems = sorted.filter(i => !i.isPrivate && !isIn(amethyst, i) && !isIn(web, i)) + return { privateItems, publicItems, web, amethyst } +} From eefcf99364416b94265b168a9f374b401a6c1448 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 13:59:00 +0200 Subject: [PATCH 04/43] feat(bookmarks): render grouped sections in sidebar with global view mode --- src/components/BookmarkList.tsx | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 6b7a6244..5363df63 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -12,6 +12,7 @@ import { extractUrlsFromContent } from '../services/bookmarkHelpers' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { BookmarkSkeleton } from './Skeletons' +import { groupIndividualBookmarks } from '../utils/bookmarkUtils' interface BookmarkListProps { bookmarks: Bookmark[] @@ -91,7 +92,13 @@ export const BookmarkList: React.FC = ({ // Re-sort after flattening to ensure newest first across all lists const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContentOrUrl) - .sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0))) + const groups = groupIndividualBookmarks(allIndividualBookmarks) + const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ + { key: 'private', title: `Private bookmarks (${groups.privateItems.length})`, items: groups.privateItems }, + { key: 'public', title: `Public bookmarks (${groups.publicItems.length})`, items: groups.publicItems }, + { key: 'web', title: `Web bookmarks (${groups.web.length})`, items: groups.web }, + { key: 'amethyst', title: `Amethyst-style bookmarks (${groups.amethyst.length})`, items: groups.amethyst } + ] if (isCollapsed) { // Check if the selected URL is in bookmarks @@ -150,17 +157,22 @@ export const BookmarkList: React.FC = ({ isRefreshing={isPulling || isRefreshing || false} pullPosition={pullPosition} /> -
- {allIndividualBookmarks.map((individualBookmark, index) => - - )} -
+ {sections.filter(s => s.items.length > 0).map(section => ( +
+

{section.title}

+
+ {section.items.map((individualBookmark, index) => ( + + ))} +
+
+ ))} )}
From d533e23dc0fffd983d16e7f56c71cc9297af08d6 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 13:59:10 +0200 Subject: [PATCH 05/43] feat(bookmarks): render grouped sections in /me reading-list with global controls --- src/components/Me.tsx | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 45876095..e1cf6de0 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -24,6 +24,7 @@ import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../ser import { faBooks } from '../icons/customIcons' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' +import { groupIndividualBookmarks } from '../utils/bookmarkUtils' interface MeProps { relayPool: RelayPool @@ -189,7 +190,13 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr // Merge and flatten all individual bookmarks (same logic as BookmarkList) const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContentOrUrl) - .sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0))) + const groups = groupIndividualBookmarks(allIndividualBookmarks) + const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ + { key: 'private', title: `Private bookmarks (${groups.privateItems.length})`, items: groups.privateItems }, + { key: 'public', title: `Public bookmarks (${groups.publicItems.length})`, items: groups.publicItems }, + { key: 'web', title: `Web bookmarks (${groups.web.length})`, items: groups.web }, + { key: 'amethyst', title: `Amethyst-style bookmarks (${groups.amethyst.length})`, items: groups.amethyst } + ] // Show content progressively - no blocking error screens const hasData = highlights.length > 0 || bookmarks.length > 0 || readArticles.length > 0 || writings.length > 0 @@ -246,17 +253,22 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
) : (
-
- {allIndividualBookmarks.map((individualBookmark, index) => ( - - ))} -
+ {sections.filter(s => s.items.length > 0).map(section => ( +
+

{section.title}

+
+ {section.items.map((individualBookmark, index) => ( + + ))} +
+
+ ))}
Date: Wed, 15 Oct 2025 14:14:54 +0200 Subject: [PATCH 06/43] refactor(bookmarks): simplify filtering to only exclude empty content --- src/components/BookmarkList.tsx | 31 ++----------------------------- src/components/Me.tsx | 24 +++--------------------- src/utils/bookmarkUtils.tsx | 5 +++++ 3 files changed, 10 insertions(+), 50 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 5363df63..978deaa7 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -8,11 +8,10 @@ import { BookmarkItem } from './BookmarkItem' import SidebarHeader from './SidebarHeader' import IconButton from './IconButton' import { ViewMode } from './Bookmarks' -import { extractUrlsFromContent } from '../services/bookmarkHelpers' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { BookmarkSkeleton } from './Skeletons' -import { groupIndividualBookmarks } from '../utils/bookmarkUtils' +import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' interface BookmarkListProps { bookmarks: Bookmark[] @@ -63,35 +62,9 @@ export const BookmarkList: React.FC = ({ isDisabled: !onRefresh }) - // Helper to check if a bookmark has either content or a URL - const hasContentOrUrl = (ib: IndividualBookmark) => { - // Check if has content (text) - const hasContent = ib.content && ib.content.trim().length > 0 - - // Check if has URL - let hasUrl = false - - // For web bookmarks (kind:39701), URL is in the 'd' tag - if (ib.kind === 39701) { - const dTag = ib.tags?.find((t: string[]) => t[0] === 'd')?.[1] - hasUrl = !!dTag && dTag.trim().length > 0 - } else { - // For other bookmarks, extract URLs from content - const urls = extractUrlsFromContent(ib.content || '') - hasUrl = urls.length > 0 - } - - // Always show articles (kind:30023) as they have special handling - if (ib.kind === 30023) return true - - // Otherwise, must have either content or URL - return hasContent || hasUrl - } - // Merge and flatten all individual bookmarks from all lists - // Re-sort after flattening to ensure newest first across all lists const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) - .filter(hasContentOrUrl) + .filter(hasContent) const groups = groupIndividualBookmarks(allIndividualBookmarks) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ { key: 'private', title: `Private bookmarks (${groups.privateItems.length})`, items: groups.privateItems }, diff --git a/src/components/Me.tsx b/src/components/Me.tsx index e1cf6de0..5c7b3501 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -19,12 +19,11 @@ import BlogPostCard from './BlogPostCard' import { BookmarkItem } from './BookmarkItem' import IconButton from './IconButton' import { ViewMode } from './Bookmarks' -import { extractUrlsFromContent } from '../services/bookmarkHelpers' import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache' import { faBooks } from '../icons/customIcons' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' -import { groupIndividualBookmarks } from '../utils/bookmarkUtils' +import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' interface MeProps { relayPool: RelayPool @@ -151,23 +150,6 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr return `/a/${naddr}` } - // Helper to check if a bookmark has either content or a URL (same logic as BookmarkList) - const hasContentOrUrl = (ib: IndividualBookmark) => { - const hasContent = ib.content && ib.content.trim().length > 0 - - let hasUrl = false - if (ib.kind === 39701) { - const dTag = ib.tags?.find((t: string[]) => t[0] === 'd')?.[1] - hasUrl = !!dTag && dTag.trim().length > 0 - } else { - const urls = extractUrlsFromContent(ib.content || '') - hasUrl = urls.length > 0 - } - - if (ib.kind === 30023) return true - return hasContent || hasUrl - } - const handleSelectUrl = (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => { if (bookmark && bookmark.kind === 30023) { // For kind:30023 articles, navigate to the article route @@ -187,9 +169,9 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr } } - // Merge and flatten all individual bookmarks (same logic as BookmarkList) + // Merge and flatten all individual bookmarks const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) - .filter(hasContentOrUrl) + .filter(hasContent) const groups = groupIndividualBookmarks(allIndividualBookmarks) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ { key: 'private', title: `Private bookmarks (${groups.privateItems.length})`, items: groups.privateItems }, diff --git a/src/utils/bookmarkUtils.tsx b/src/utils/bookmarkUtils.tsx index 501ed2f4..6c5b7775 100644 --- a/src/utils/bookmarkUtils.tsx +++ b/src/utils/bookmarkUtils.tsx @@ -99,3 +99,8 @@ export function groupIndividualBookmarks(items: IndividualBookmark[]) { const publicItems = sorted.filter(i => !i.isPrivate && !isIn(amethyst, i) && !isIn(web, i)) return { privateItems, publicItems, web, amethyst } } + +// Simple filter: only exclude bookmarks with empty/whitespace-only content +export function hasContent(bookmark: IndividualBookmark): boolean { + return !!(bookmark.content && bookmark.content.trim().length > 0) +} From 838bb6aa3d2447d474909c08b3888792621ec3b1 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:15:01 +0200 Subject: [PATCH 07/43] feat(bookmarks): add content type icons to indicate article/video/web --- src/components/BookmarkItem.tsx | 14 ++++++++++--- src/components/BookmarkViews/CardView.tsx | 22 +++++++------------- src/components/BookmarkViews/CompactView.tsx | 22 +++++++------------- src/components/BookmarkViews/LargeView.tsx | 12 ++++++++++- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index bd97cbdd..09b018a2 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' -import { faBookOpen, faPlay, faEye } from '@fortawesome/free-solid-svg-icons' +import { faBookOpen, faPlay, faEye, faNewspaper, faGlobe } from '@fortawesome/free-solid-svg-icons' +import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' import { npubEncode, neventEncode } from 'nostr-tools/nip19' @@ -66,7 +67,13 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS return short(bookmark.pubkey) // fallback to short pubkey } - // use helper from kindIcon.ts + // Get content type icon based on bookmark kind and URL classification + const getContentTypeIcon = (): IconDefinition => { + if (isArticle) return faNewspaper + if (isWebBookmark) return faGlobe + if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faPlay + return faBookOpen + } const getIconForUrlType = (url: string) => { const classification = classifyUrl(url) @@ -113,7 +120,8 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS getAuthorDisplayName, handleReadNow, articleImage, - articleSummary + articleSummary, + contentTypeIcon: getContentTypeIcon() } if (viewMode === 'compact') { diff --git a/src/components/BookmarkViews/CardView.tsx b/src/components/BookmarkViews/CardView.tsx index 8af56045..cd92b2cf 100644 --- a/src/components/BookmarkViews/CardView.tsx +++ b/src/components/BookmarkViews/CardView.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react' import { Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookmark, faUserLock, faChevronDown, faChevronUp, faGlobe } from '@fortawesome/free-solid-svg-icons' +import { faUserLock, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons' +import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDate, renderParsedContent } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' @@ -25,6 +26,7 @@ interface CardViewProps { handleReadNow: (e: React.MouseEvent) => void articleImage?: string articleSummary?: string + contentTypeIcon: IconDefinition } export const CardView: React.FC = ({ @@ -39,7 +41,8 @@ export const CardView: React.FC = ({ getAuthorDisplayName, handleReadNow, articleImage, - articleSummary + articleSummary, + contentTypeIcon }) => { const firstUrl = hasUrls ? extractedUrls[0] : null const firstUrlClassificationType = firstUrl ? classifyUrl(firstUrl)?.type : null @@ -92,18 +95,9 @@ export const CardView: React.FC = ({ )}
- {isWebBookmark ? ( - - - - - ) : bookmark.isPrivate ? ( - <> - - - - ) : ( - + + {bookmark.isPrivate && ( + )} diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index f4d61499..c11b8e60 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -1,6 +1,7 @@ import React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookmark, faUserLock, faGlobe } from '@fortawesome/free-solid-svg-icons' +import { faBookmark, faUserLock } from '@fortawesome/free-solid-svg-icons' +import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDateCompact } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' @@ -14,6 +15,7 @@ interface CompactViewProps { onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void articleImage?: string articleSummary?: string + contentTypeIcon: IconDefinition } export const CompactView: React.FC = ({ @@ -23,7 +25,8 @@ export const CompactView: React.FC = ({ extractedUrls, onSelectUrl, articleImage, - articleSummary + articleSummary, + contentTypeIcon }) => { const isArticle = bookmark.kind === 30023 const isWebBookmark = bookmark.kind === 39701 @@ -63,18 +66,9 @@ export const CompactView: React.FC = ({ )} - {isWebBookmark ? ( - - - - - ) : bookmark.isPrivate ? ( - <> - - - - ) : ( - + + {bookmark.isPrivate && ( + )} {displayText && ( diff --git a/src/components/BookmarkViews/LargeView.tsx b/src/components/BookmarkViews/LargeView.tsx index 80001976..106b85a0 100644 --- a/src/components/BookmarkViews/LargeView.tsx +++ b/src/components/BookmarkViews/LargeView.tsx @@ -1,6 +1,8 @@ import React from 'react' import { Link } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faUserLock } from '@fortawesome/free-solid-svg-icons' +import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDate } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' @@ -21,6 +23,7 @@ interface LargeViewProps { getAuthorDisplayName: () => string handleReadNow: (e: React.MouseEvent) => void articleSummary?: string + contentTypeIcon: IconDefinition } export const LargeView: React.FC = ({ @@ -35,7 +38,8 @@ export const LargeView: React.FC = ({ eventNevent, getAuthorDisplayName, handleReadNow, - articleSummary + articleSummary, + contentTypeIcon }) => { const cachedImage = useImageCache(previewImage || undefined) const isArticle = bookmark.kind === 30023 @@ -90,6 +94,12 @@ export const LargeView: React.FC = ({ )}
+ + + {bookmark.isPrivate && ( + + )} + Date: Wed, 15 Oct 2025 14:20:28 +0200 Subject: [PATCH 08/43] feat(bookmarks): add sticky note icon for text-only bookmarks without URLs --- src/components/BookmarkItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 09b018a2..63e7a3b9 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { faBookOpen, faPlay, faEye, faNewspaper, faGlobe } from '@fortawesome/free-solid-svg-icons' +import { faBookOpen, faPlay, faEye, faNewspaper, faGlobe, faStickyNote } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' @@ -71,6 +71,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS const getContentTypeIcon = (): IconDefinition => { if (isArticle) return faNewspaper if (isWebBookmark) return faGlobe + if (!hasUrls) return faStickyNote // Just a text note if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faPlay return faBookOpen } From 300aed058970a02213e0adea2abd5fdb39146453 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:21:09 +0200 Subject: [PATCH 09/43] style(bookmarks): use regular icon variants for lighter appearance --- src/components/BookmarkItem.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 63e7a3b9..72d9e6f2 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' -import { faBookOpen, faPlay, faEye, faNewspaper, faGlobe, faStickyNote } from '@fortawesome/free-solid-svg-icons' +import { faBookOpen, faNewspaper, faStickyNote } from '@fortawesome/free-regular-svg-icons' +import { faPlay, faEye, faGlobe } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' From 081bd95f605a0f288a14c7543cddb7427030a04f Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:22:13 +0200 Subject: [PATCH 10/43] feat(bookmarks): classify web bookmark URLs to show appropriate content icons --- src/components/BookmarkItem.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 72d9e6f2..0a2363f0 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -71,7 +71,22 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS // Get content type icon based on bookmark kind and URL classification const getContentTypeIcon = (): IconDefinition => { if (isArticle) return faNewspaper - if (isWebBookmark) return faGlobe + + // For web bookmarks, classify the URL to determine icon + if (isWebBookmark && firstUrlClassification) { + switch (firstUrlClassification.type) { + case 'youtube': + case 'video': + return faPlay + case 'image': + return faEye + case 'article': + return faNewspaper + default: + return faGlobe + } + } + if (!hasUrls) return faStickyNote // Just a text note if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faPlay return faBookOpen From 142a2414d37d15dc75e67528669321fb42af7544 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:24:07 +0200 Subject: [PATCH 11/43] style(bookmarks): use regular icon variants for all classification icons --- src/components/BookmarkItem.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 0a2363f0..b4e6aa9c 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' -import { faBookOpen, faNewspaper, faStickyNote } from '@fortawesome/free-regular-svg-icons' -import { faPlay, faEye, faGlobe } from '@fortawesome/free-solid-svg-icons' +import { faBookOpen, faNewspaper, faStickyNote, faCirclePlay, faEye } from '@fortawesome/free-regular-svg-icons' +import { faGlobe } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' @@ -77,7 +77,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS switch (firstUrlClassification.type) { case 'youtube': case 'video': - return faPlay + return faCirclePlay case 'image': return faEye case 'article': @@ -88,7 +88,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS } if (!hasUrls) return faStickyNote // Just a text note - if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faPlay + if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faCirclePlay return faBookOpen } @@ -97,7 +97,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS switch (classification.type) { case 'youtube': case 'video': - return faPlay + return faCirclePlay case 'image': return faEye default: From 82ab8419e35c4fe7223fcf3d5998a2154c8c2e3f Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:26:02 +0200 Subject: [PATCH 12/43] fix(lint): remove unused variables and fix icon imports --- package-lock.json | 4 ++-- src/components/BookmarkItem.tsx | 8 ++++---- src/components/BookmarkViews/CardView.tsx | 1 - src/components/BookmarkViews/CompactView.tsx | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba5f7236..64f8bcbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "boris", - "version": "0.6.9", + "version": "0.6.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "boris", - "version": "0.6.9", + "version": "0.6.13", "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index b4e6aa9c..2c91d07f 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' -import { faBookOpen, faNewspaper, faStickyNote, faCirclePlay, faEye } from '@fortawesome/free-regular-svg-icons' -import { faGlobe } from '@fortawesome/free-solid-svg-icons' +import { faNewspaper, faStickyNote, faCirclePlay, faEye } from '@fortawesome/free-regular-svg-icons' +import { faBook, faGlobe } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' @@ -89,7 +89,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS if (!hasUrls) return faStickyNote // Just a text note if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faCirclePlay - return faBookOpen + return faBook } const getIconForUrlType = (url: string) => { @@ -101,7 +101,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS case 'image': return faEye default: - return faBookOpen + return faBook } } diff --git a/src/components/BookmarkViews/CardView.tsx b/src/components/BookmarkViews/CardView.tsx index cd92b2cf..5dcfb540 100644 --- a/src/components/BookmarkViews/CardView.tsx +++ b/src/components/BookmarkViews/CardView.tsx @@ -55,7 +55,6 @@ export const CardView: React.FC = ({ const contentLength = (bookmark.content || '').length const shouldTruncate = !expanded && contentLength > 210 const isArticle = bookmark.kind === 30023 - const isWebBookmark = bookmark.kind === 39701 // Determine which image to use (article image, instant preview, or OG image) const previewImage = articleImage || instantPreview || ogImage diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index c11b8e60..d7abf29e 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -1,6 +1,6 @@ import React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookmark, faUserLock } from '@fortawesome/free-solid-svg-icons' +import { faUserLock } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDateCompact } from '../../utils/bookmarkUtils' From b48397b7a606eebf0112f2ff4c96877687f46232 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:29:35 +0200 Subject: [PATCH 13/43] feat(bookmarks): use camera icon for image bookmarks --- src/components/BookmarkItem.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 2c91d07f..17a9310f 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { faNewspaper, faStickyNote, faCirclePlay, faEye } from '@fortawesome/free-regular-svg-icons' +import { faNewspaper, faStickyNote, faCirclePlay, faCamera } from '@fortawesome/free-regular-svg-icons' import { faBook, faGlobe } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' @@ -79,7 +79,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS case 'video': return faCirclePlay case 'image': - return faEye + return faCamera case 'article': return faNewspaper default: @@ -99,7 +99,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS case 'video': return faCirclePlay case 'image': - return faEye + return faCamera default: return faBook } From cb0066aac9c4c7e5f43acb72d53385aa36b329f3 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:31:27 +0200 Subject: [PATCH 14/43] style(bookmarks): use file-lines icon instead of book for default bookmarks --- src/components/BookmarkItem.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 17a9310f..d8ff642d 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' -import { faNewspaper, faStickyNote, faCirclePlay, faCamera } from '@fortawesome/free-regular-svg-icons' -import { faBook, faGlobe } from '@fortawesome/free-solid-svg-icons' +import { faNewspaper, faStickyNote, faCirclePlay, faCamera, faFileLines } from '@fortawesome/free-regular-svg-icons' +import { faGlobe } from '@fortawesome/free-solid-svg-icons' import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' @@ -89,7 +89,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS if (!hasUrls) return faStickyNote // Just a text note if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faCirclePlay - return faBook + return faFileLines } const getIconForUrlType = (url: string) => { @@ -101,7 +101,7 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS case 'image': return faCamera default: - return faBook + return faFileLines } } From d7f90faea912700cbf7ec9f63e0f01587c2f84e5 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:35:18 +0200 Subject: [PATCH 15/43] style(bookmarks): improve section headings with better typography and remove counts --- src/components/BookmarkList.tsx | 8 ++++---- src/components/Me.tsx | 8 ++++---- src/styles/components/cards.css | 3 +++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 978deaa7..4fc04c93 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -67,10 +67,10 @@ export const BookmarkList: React.FC = ({ .filter(hasContent) const groups = groupIndividualBookmarks(allIndividualBookmarks) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ - { key: 'private', title: `Private bookmarks (${groups.privateItems.length})`, items: groups.privateItems }, - { key: 'public', title: `Public bookmarks (${groups.publicItems.length})`, items: groups.publicItems }, - { key: 'web', title: `Web bookmarks (${groups.web.length})`, items: groups.web }, - { key: 'amethyst', title: `Amethyst-style bookmarks (${groups.amethyst.length})`, items: groups.amethyst } + { key: 'private', title: 'Private bookmarks', items: groups.privateItems }, + { key: 'public', title: 'Public bookmarks', items: groups.publicItems }, + { key: 'web', title: 'Web bookmarks', items: groups.web }, + { key: 'amethyst', title: 'Amethyst-style bookmarks', items: groups.amethyst } ] if (isCollapsed) { diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 5c7b3501..7ebda014 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -174,10 +174,10 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr .filter(hasContent) const groups = groupIndividualBookmarks(allIndividualBookmarks) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ - { key: 'private', title: `Private bookmarks (${groups.privateItems.length})`, items: groups.privateItems }, - { key: 'public', title: `Public bookmarks (${groups.publicItems.length})`, items: groups.publicItems }, - { key: 'web', title: `Web bookmarks (${groups.web.length})`, items: groups.web }, - { key: 'amethyst', title: `Amethyst-style bookmarks (${groups.amethyst.length})`, items: groups.amethyst } + { key: 'private', title: 'Private bookmarks', items: groups.privateItems }, + { key: 'public', title: 'Public bookmarks', items: groups.publicItems }, + { key: 'web', title: 'Web bookmarks', items: groups.web }, + { key: 'amethyst', title: 'Amethyst-style bookmarks', items: groups.amethyst } ] // Show content progressively - no blocking error screens diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index 2e59cc12..0480af8f 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -7,6 +7,9 @@ .bookmark-content { color: var(--color-text); margin: 0.5rem 0; line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; } .bookmark-meta { color: var(--color-text-secondary); font-size: 0.9rem; margin-top: 0.5rem; } +.bookmarks-section-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-muted); padding: 1.5rem 0.5rem 0.75rem; margin: 0; border-top: 1px solid var(--color-border); } +.bookmarks-section-title:first-child { border-top: none; padding-top: 0.5rem; } + .individual-bookmarks { margin: 1rem 0; } .individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--color-text); } From 7d3748202e0bad845759c83ec9ffa2c311da9db0 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:39:00 +0200 Subject: [PATCH 16/43] fix(bookmarks): ensure section heading styles override with important --- src/styles/components/cards.css | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index 0480af8f..0a4c02a8 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -7,8 +7,20 @@ .bookmark-content { color: var(--color-text); margin: 0.5rem 0; line-height: 1.4; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; } .bookmark-meta { color: var(--color-text-secondary); font-size: 0.9rem; margin-top: 0.5rem; } -.bookmarks-section-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-muted); padding: 1.5rem 0.5rem 0.75rem; margin: 0; border-top: 1px solid var(--color-border); } -.bookmarks-section-title:first-child { border-top: none; padding-top: 0.5rem; } +.bookmarks-section-title { + font-size: 0.75rem !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + color: var(--color-text-muted) !important; + padding: 1.5rem 0.5rem 0.75rem !important; + margin: 0 !important; + border-top: 1px solid var(--color-border); +} +.bookmarks-section:first-of-type .bookmarks-section-title { + border-top: none; + padding-top: 0.5rem !important; +} .individual-bookmarks { margin: 1rem 0; } .individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--color-text); } From cf2a500a071ac0516e2c5f768b8490b7dcc2d976 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:39:43 +0200 Subject: [PATCH 17/43] style(bookmarks): remove border from compact view bookmarks --- src/styles/components/cards.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index 0a4c02a8..20726263 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -38,8 +38,8 @@ .individual-bookmark:hover { border-color: var(--color-border); background: var(--color-bg-elevated); } /* Compact view */ -.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-bottom: 1px solid var(--color-bg-elevated); border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; } -.individual-bookmark.compact:hover { background: var(--color-bg-elevated); border-bottom-color: var(--color-border); transform: none; box-shadow: none; } +.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; } +.individual-bookmark.compact:hover { background: var(--color-bg-elevated); transform: none; box-shadow: none; } .compact-row { display: flex; align-items: center; gap: 0.5rem; height: 28px; width: 100%; min-width: 0; overflow: hidden; } .compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: var(--color-bg-elevated); display: flex; align-items: center; justify-content: center; } .compact-thumbnail img { width: 100%; height: 100%; object-fit: cover; } From 03923893552aea842423da878167b9c89d437bca Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:57:24 +0200 Subject: [PATCH 18/43] style: change support button icon from lightning bolt to heart --- src/components/Support.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Support.tsx b/src/components/Support.tsx index 9c211d19..9449895e 100644 --- a/src/components/Support.tsx +++ b/src/components/Support.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' import { RelayPool } from 'applesauce-relay' import { IEventStore } from 'applesauce-core' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBolt, faSpinner, faUserCircle } from '@fortawesome/free-solid-svg-icons' +import { faHeart, faSpinner, faUserCircle } from '@fortawesome/free-solid-svg-icons' import { fetchBorisZappers, ZapSender } from '../services/zapReceiptService' import { fetchProfiles } from '../services/profileService' import { UserSettings } from '../services/settingsService' @@ -207,7 +207,7 @@ const SupporterCard: React.FC = ({ supporter, isWhale }) => className="absolute -bottom-1 -right-1 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center border-2" style={{ borderColor: 'var(--color-bg)' }} > - +
)}
From 1d1d389a03d4596447ecfd2628066fe3e396fb36 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 14:58:43 +0200 Subject: [PATCH 19/43] feat: move support button to bottom-left of bookmarks bar --- src/components/BookmarkList.tsx | 11 ++++++++++- src/components/SidebarHeader.tsx | 9 +-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 4fc04c93..4ed4de1a 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -1,6 +1,7 @@ import React, { useRef } from 'react' +import { useNavigate } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faChevronLeft, faBookmark, faList, faThLarge, faImage, faRotate } from '@fortawesome/free-solid-svg-icons' +import { faChevronLeft, faBookmark, faList, faThLarge, faImage, faRotate, faHeart } from '@fortawesome/free-solid-svg-icons' import { formatDistanceToNow } from 'date-fns' import { RelayPool } from 'applesauce-relay' import { Bookmark, IndividualBookmark } from '../types/bookmarks' @@ -48,6 +49,7 @@ export const BookmarkList: React.FC = ({ relayPool, isMobile = false }) => { + const navigate = useNavigate() const bookmarksListRef = useRef(null) // Pull-to-refresh for bookmarks @@ -149,6 +151,13 @@ export const BookmarkList: React.FC = ({
)}
+ navigate('/support')} + title="Support Boris" + ariaLabel="Support" + variant="ghost" + /> {onRefresh && ( = ({ onToggleCollapse, onLogou ariaLabel="Explore" variant="ghost" /> - navigate('/support')} - title="Support" - ariaLabel="Support" - variant="ghost" - /> Date: Wed, 15 Oct 2025 14:59:51 +0200 Subject: [PATCH 20/43] style: make support heart icon orange using friends color from settings --- src/components/BookmarkList.tsx | 7 ++++++- src/components/ThreePaneLayout.tsx | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 4ed4de1a..28494107 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -13,6 +13,7 @@ import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { BookmarkSkeleton } from './Skeletons' import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' +import { UserSettings } from '../services/settingsService' interface BookmarkListProps { bookmarks: Bookmark[] @@ -30,6 +31,7 @@ interface BookmarkListProps { loading?: boolean relayPool: RelayPool | null isMobile?: boolean + settings?: UserSettings } export const BookmarkList: React.FC = ({ @@ -47,10 +49,12 @@ export const BookmarkList: React.FC = ({ lastFetchTime, loading = false, relayPool, - isMobile = false + isMobile = false, + settings }) => { const navigate = useNavigate() const bookmarksListRef = useRef(null) + const friendsColor = settings?.highlightColorFriends || '#f97316' // Pull-to-refresh for bookmarks const { isRefreshing: isPulling, pullPosition } = usePullToRefresh({ @@ -157,6 +161,7 @@ export const BookmarkList: React.FC = ({ title="Support Boris" ariaLabel="Support" variant="ghost" + style={{ color: friendsColor }} /> {onRefresh && ( = (props) => { loading={props.bookmarksLoading} relayPool={props.relayPool} isMobile={isMobile} + settings={props.settings} />
Date: Wed, 15 Oct 2025 15:01:25 +0200 Subject: [PATCH 21/43] style: left-align support button, right-align view mode buttons --- src/components/BookmarkList.tsx | 78 +++++++++++++++++---------------- src/styles/layout/sidebar.css | 9 +++- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 28494107..91230305 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -155,46 +155,50 @@ export const BookmarkList: React.FC = ({
)}
- navigate('/support')} - title="Support Boris" - ariaLabel="Support" - variant="ghost" - style={{ color: friendsColor }} - /> - {onRefresh && ( +
navigate('/support')} + title="Support Boris" + ariaLabel="Support" variant="ghost" - disabled={isRefreshing} - spin={isRefreshing} + style={{ color: friendsColor }} /> - )} - onViewModeChange('compact')} - title="Compact list view" - ariaLabel="Compact list view" - variant={viewMode === 'compact' ? 'primary' : 'ghost'} - /> - onViewModeChange('cards')} - title="Cards view" - ariaLabel="Cards view" - variant={viewMode === 'cards' ? 'primary' : 'ghost'} - /> - onViewModeChange('large')} - title="Large preview view" - ariaLabel="Large preview view" - variant={viewMode === 'large' ? 'primary' : 'ghost'} - /> +
+
+ {onRefresh && ( + + )} + onViewModeChange('compact')} + title="Compact list view" + ariaLabel="Compact list view" + variant={viewMode === 'compact' ? 'primary' : 'ghost'} + /> + onViewModeChange('cards')} + title="Cards view" + ariaLabel="Cards view" + variant={viewMode === 'cards' ? 'primary' : 'ghost'} + /> + onViewModeChange('large')} + title="Large preview view" + ariaLabel="Large preview view" + variant={viewMode === 'large' ? 'primary' : 'ghost'} + /> +
) diff --git a/src/styles/layout/sidebar.css b/src/styles/layout/sidebar.css index 140415ee..2013ed7c 100644 --- a/src/styles/layout/sidebar.css +++ b/src/styles/layout/sidebar.css @@ -81,7 +81,14 @@ .view-mode-controls { display: flex; align-items: center; - justify-content: center; + justify-content: space-between; + gap: 0.5rem; +} + +.view-mode-left, +.view-mode-right { + display: flex; + align-items: center; gap: 0.5rem; } From 715fd8cf107f4bd7baafa7982fcb563cabb588b6 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:11:00 +0200 Subject: [PATCH 22/43] refactor: remove duplicate type indicator icon from bookmark cards --- src/components/BookmarkItem.tsx | 2 +- src/components/BookmarkViews/CardView.tsx | 29 +++++++---------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index d8ff642d..381678aa 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -150,5 +150,5 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS return } - return + return } diff --git a/src/components/BookmarkViews/CardView.tsx b/src/components/BookmarkViews/CardView.tsx index 5dcfb540..d73dde29 100644 --- a/src/components/BookmarkViews/CardView.tsx +++ b/src/components/BookmarkViews/CardView.tsx @@ -6,9 +6,7 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDate, renderParsedContent } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' -import IconButton from '../IconButton' import { classifyUrl } from '../../utils/helpers' -import { IconGetter } from './shared' import { useImageCache } from '../../hooks/useImageCache' import { getPreviewImage, fetchOgImage } from '../../utils/imagePreview' import { getEventUrl } from '../../config/nostrGateways' @@ -19,7 +17,6 @@ interface CardViewProps { hasUrls: boolean extractedUrls: string[] onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void - getIconForUrlType: IconGetter authorNpub: string eventNevent?: string getAuthorDisplayName: () => string @@ -35,7 +32,6 @@ export const CardView: React.FC = ({ hasUrls, extractedUrls, onSelectUrl, - getIconForUrlType, authorNpub, eventNevent, getAuthorDisplayName, @@ -120,23 +116,14 @@ export const CardView: React.FC = ({
{(urlsExpanded ? extractedUrls : extractedUrls.slice(0, 1)).map((url, urlIndex) => { return ( -
- - { e.preventDefault(); e.stopPropagation(); onSelectUrl?.(url) }} - /> -
+ ) })} {extractedUrls.length > 1 && ( From eaa590b8e289da16cf6361f192e825dd468f98e6 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:25:33 +0200 Subject: [PATCH 23/43] feat: add support for bookmark sets (kind 30003) - Add setName, setTitle, setDescription, and setImage fields to IndividualBookmark type - Extract d tag and metadata from kind 30003 events in bookmark processing - Create helper functions to group bookmarks by set and extract set metadata - Display bookmark sets as separate sections in BookmarkList UI - Maintain existing content-type categorization alongside bookmark sets --- src/components/BookmarkList.tsx | 19 ++++++++++-- src/services/bookmarkProcessing.ts | 30 ++++++++++++++++--- src/types/bookmarks.ts | 6 ++++ src/utils/bookmarkUtils.tsx | 46 ++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 91230305..e2f0e092 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -12,7 +12,7 @@ import { ViewMode } from './Bookmarks' import { usePullToRefresh } from 'use-pull-to-refresh' import RefreshIndicator from './RefreshIndicator' import { BookmarkSkeleton } from './Skeletons' -import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils' +import { groupIndividualBookmarks, hasContent, getBookmarkSets, getBookmarksWithoutSet } from '../utils/bookmarkUtils' import { UserSettings } from '../services/settingsService' interface BookmarkListProps { @@ -71,7 +71,13 @@ export const BookmarkList: React.FC = ({ // Merge and flatten all individual bookmarks from all lists const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || []) .filter(hasContent) - const groups = groupIndividualBookmarks(allIndividualBookmarks) + + // Separate bookmarks with setName (kind 30003) from regular bookmarks + const bookmarksWithoutSet = getBookmarksWithoutSet(allIndividualBookmarks) + const bookmarkSets = getBookmarkSets(allIndividualBookmarks) + + // Group non-set bookmarks as before + const groups = groupIndividualBookmarks(bookmarksWithoutSet) const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = [ { key: 'private', title: 'Private bookmarks', items: groups.privateItems }, { key: 'public', title: 'Public bookmarks', items: groups.publicItems }, @@ -79,6 +85,15 @@ export const BookmarkList: React.FC = ({ { key: 'amethyst', title: 'Amethyst-style bookmarks', items: groups.amethyst } ] + // Add bookmark sets as additional sections + bookmarkSets.forEach(set => { + sections.push({ + key: `set-${set.name}`, + title: set.title || set.name, + items: set.bookmarks + }) + }) + if (isCollapsed) { // Check if the selected URL is in bookmarks const isBookmarked = selectedUrl && bookmarks.some(bookmark => { diff --git a/src/services/bookmarkProcessing.ts b/src/services/bookmarkProcessing.ts index 645391c4..888699c0 100644 --- a/src/services/bookmarkProcessing.ts +++ b/src/services/bookmarkProcessing.ts @@ -33,6 +33,12 @@ export async function collectBookmarksFromEvents( if (!latestContent && evt.content && !Helpers.hasHiddenContent(evt)) latestContent = evt.content if (Array.isArray(evt.tags)) allTags = allTags.concat(evt.tags) + // Extract the 'd' tag and metadata for bookmark sets (kind 30003) + const dTag = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] : undefined + const setTitle = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'title')?.[1] : undefined + const setDescription = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'description')?.[1] : undefined + const setImage = evt.kind === 30003 ? evt.tags?.find((t: string[]) => t[0] === 'image')?.[1] : undefined + // Handle web bookmarks (kind:39701) as individual bookmarks if (evt.kind === 39701) { publicItemsAll.push({ @@ -46,7 +52,11 @@ export async function collectBookmarksFromEvents( type: 'web' as const, isPrivate: false, added_at: evt.created_at || Math.floor(Date.now() / 1000), - sourceKind: 39701 + sourceKind: 39701, + setName: dTag, + setTitle, + setDescription, + setImage }) continue } @@ -55,7 +65,11 @@ export async function collectBookmarksFromEvents( publicItemsAll.push( ...processApplesauceBookmarks(pub, activeAccount, false).map(i => ({ ...i, - sourceKind: evt.kind + sourceKind: evt.kind, + setName: dTag, + setTitle, + setDescription, + setImage })) ) @@ -103,7 +117,11 @@ export async function collectBookmarksFromEvents( privateItemsAll.push( ...processApplesauceBookmarks(manualPrivate, activeAccount, true).map(i => ({ ...i, - sourceKind: evt.kind + sourceKind: evt.kind, + setName: dTag, + setTitle, + setDescription, + setImage })) ) Reflect.set(evt, BookmarkHiddenSymbol, manualPrivate) @@ -120,7 +138,11 @@ export async function collectBookmarksFromEvents( privateItemsAll.push( ...processApplesauceBookmarks(priv, activeAccount, true).map(i => ({ ...i, - sourceKind: evt.kind + sourceKind: evt.kind, + setName: dTag, + setTitle, + setDescription, + setImage })) ) } diff --git a/src/types/bookmarks.ts b/src/types/bookmarks.ts index 7cb7c8ea..94d6ec05 100644 --- a/src/types/bookmarks.ts +++ b/src/types/bookmarks.ts @@ -44,6 +44,12 @@ export interface IndividualBookmark { added_at?: number // The kind of the source list/set that produced this bookmark (e.g., 10003, 30003, 30001, or 39701 for web) sourceKind?: number + // The 'd' tag value from kind 30003 bookmark sets + setName?: string + // Metadata from the bookmark set event (kind 30003) + setTitle?: string + setDescription?: string + setImage?: string } export interface ActiveAccount { diff --git a/src/utils/bookmarkUtils.tsx b/src/utils/bookmarkUtils.tsx index 6c5b7775..14e0716d 100644 --- a/src/utils/bookmarkUtils.tsx +++ b/src/utils/bookmarkUtils.tsx @@ -104,3 +104,49 @@ export function groupIndividualBookmarks(items: IndividualBookmark[]) { export function hasContent(bookmark: IndividualBookmark): boolean { return !!(bookmark.content && bookmark.content.trim().length > 0) } + +// Bookmark sets helpers (kind 30003) +export interface BookmarkSet { + name: string + title?: string + description?: string + image?: string + bookmarks: IndividualBookmark[] +} + +export function getBookmarkSets(items: IndividualBookmark[]): BookmarkSet[] { + // Group bookmarks by setName + const setMap = new Map() + + items.forEach(bookmark => { + if (bookmark.setName) { + const existing = setMap.get(bookmark.setName) || [] + existing.push(bookmark) + setMap.set(bookmark.setName, existing) + } + }) + + // Convert to array and extract metadata from the bookmarks + const sets: BookmarkSet[] = [] + setMap.forEach((bookmarks, name) => { + // Get metadata from the first bookmark (all bookmarks in a set share the same metadata) + const firstBookmark = bookmarks[0] + const title = firstBookmark?.setTitle + const description = firstBookmark?.setDescription + const image = firstBookmark?.setImage + + sets.push({ + name, + title, + description, + image, + bookmarks: sortIndividualBookmarks(bookmarks) + }) + }) + + return sets.sort((a, b) => a.name.localeCompare(b.name)) +} + +export function getBookmarksWithoutSet(items: IndividualBookmark[]): IndividualBookmark[] { + return sortIndividualBookmarks(items.filter(b => !b.setName)) +} From 99d77054046d031c4dbd126001696e205c7a788b Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:40:57 +0200 Subject: [PATCH 24/43] feat: add adaptive text color for publication date over images - Install fast-average-color library for image color detection - Create useAdaptiveTextColor hook to analyze top-right image corner - Update ReaderHeader to dynamically adjust date text/shadow colors - Ensures publication date is readable on both light and dark backgrounds --- package-lock.json | 10 ++++ package.json | 1 + src/components/ReaderHeader.tsx | 18 ++++++- src/hooks/useAdaptiveTextColor.ts | 88 +++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useAdaptiveTextColor.ts diff --git a/package-lock.json b/package-lock.json index 64f8bcbc..b8ad7346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "applesauce-react": "^4.0.0", "applesauce-relay": "^4.0.0", "date-fns": "^4.1.0", + "fast-average-color": "^9.5.0", "nostr-tools": "^2.4.0", "prismjs": "^1.30.0", "react": "^18.2.0", @@ -6086,6 +6087,15 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-average-color": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/fast-average-color/-/fast-average-color-9.5.0.tgz", + "integrity": "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 524c74bd..1b390f08 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "applesauce-react": "^4.0.0", "applesauce-relay": "^4.0.0", "date-fns": "^4.1.0", + "fast-average-color": "^9.5.0", "nostr-tools": "^2.4.0", "prismjs": "^1.30.0", "react": "^18.2.0", diff --git a/src/components/ReaderHeader.tsx b/src/components/ReaderHeader.tsx index c98aad09..618c5bfb 100644 --- a/src/components/ReaderHeader.tsx +++ b/src/components/ReaderHeader.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faHighlighter, faClock, faNewspaper } from '@fortawesome/free-solid-svg-icons' import { format } from 'date-fns' import { useImageCache } from '../hooks/useImageCache' +import { useAdaptiveTextColor } from '../hooks/useAdaptiveTextColor' import { UserSettings } from '../services/settingsService' import { Highlight, HighlightLevel } from '../types/highlights' import { HighlightVisibility } from './HighlightsPanel' @@ -34,6 +35,7 @@ const ReaderHeader: React.FC = ({ highlightVisibility = { nostrverse: true, friends: true, mine: true } }) => { const cachedImage = useImageCache(image) + const { textColor, shadowColor } = useAdaptiveTextColor(cachedImage) const formattedDate = published ? format(new Date(published * 1000), 'MMM d, yyyy') : null const isLongSummary = summary && summary.length > 150 @@ -83,7 +85,13 @@ const ReaderHeader: React.FC = ({
)} {formattedDate && ( -
+
{formattedDate}
)} @@ -125,7 +133,13 @@ const ReaderHeader: React.FC = ({ {title && (
{formattedDate && ( -
+
{formattedDate}
)} diff --git a/src/hooks/useAdaptiveTextColor.ts b/src/hooks/useAdaptiveTextColor.ts new file mode 100644 index 00000000..2fb8fd46 --- /dev/null +++ b/src/hooks/useAdaptiveTextColor.ts @@ -0,0 +1,88 @@ +import { useEffect, useState } from 'react' +import FastAverageColor from 'fast-average-color' + +interface AdaptiveTextColor { + textColor: string + shadowColor: string +} + +/** + * Hook to determine optimal text and shadow colors based on image background + * Samples the top-right corner of the image to ensure publication date is readable + * + * @param imageUrl - The URL of the image to analyze + * @returns Object containing textColor and shadowColor for optimal contrast + */ +export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveTextColor { + const [colors, setColors] = useState({ + textColor: '#ffffff', + shadowColor: 'rgba(0, 0, 0, 0.5)' + }) + + useEffect(() => { + if (!imageUrl) { + // No image, use default white text + setColors({ + textColor: '#ffffff', + shadowColor: 'rgba(0, 0, 0, 0.5)' + }) + return + } + + const fac = new FastAverageColor() + const img = new Image() + img.crossOrigin = 'anonymous' + + img.onload = async () => { + try { + const width = img.naturalWidth + const height = img.naturalHeight + + // Sample top-right corner (last 25% width, first 25% height) + const color = await fac.getColor(img, { + left: Math.floor(width * 0.75), + top: 0, + width: Math.floor(width * 0.25), + height: Math.floor(height * 0.25) + }) + + // Use library's built-in isLight check for optimal contrast + if (color.isLight) { + setColors({ + textColor: '#000000', + shadowColor: 'rgba(255, 255, 255, 0.5)' + }) + } else { + setColors({ + textColor: '#ffffff', + shadowColor: 'rgba(0, 0, 0, 0.5)' + }) + } + } catch (error) { + // Fallback to default on error + console.error('Error analyzing image color:', error) + setColors({ + textColor: '#ffffff', + shadowColor: 'rgba(0, 0, 0, 0.5)' + }) + } + } + + img.onerror = () => { + // Fallback to default if image fails to load + setColors({ + textColor: '#ffffff', + shadowColor: 'rgba(0, 0, 0, 0.5)' + }) + } + + img.src = imageUrl + + return () => { + fac.destroy() + } + }, [imageUrl]) + + return colors +} + From 9b54fa9c14cff9c7e876dbae2cf2695df4a7ec95 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:48:02 +0200 Subject: [PATCH 25/43] fix: correct FastAverageColor import to use named export - Change from default import to named import - Resolves TypeScript error TS2351 --- src/hooks/useAdaptiveTextColor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAdaptiveTextColor.ts b/src/hooks/useAdaptiveTextColor.ts index 2fb8fd46..759082d8 100644 --- a/src/hooks/useAdaptiveTextColor.ts +++ b/src/hooks/useAdaptiveTextColor.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import FastAverageColor from 'fast-average-color' +import { FastAverageColor } from 'fast-average-color' interface AdaptiveTextColor { textColor: string From 8c151a5855f747fd813070f57272f230186490f5 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:49:45 +0200 Subject: [PATCH 26/43] fix: correct async handling in adaptive color detection - Remove incorrect await on synchronous getColor method - Add console logging to debug color detection - This should fix black-on-black readability issues --- src/hooks/useAdaptiveTextColor.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/hooks/useAdaptiveTextColor.ts b/src/hooks/useAdaptiveTextColor.ts index 759082d8..75e5564e 100644 --- a/src/hooks/useAdaptiveTextColor.ts +++ b/src/hooks/useAdaptiveTextColor.ts @@ -33,26 +33,35 @@ export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveText const img = new Image() img.crossOrigin = 'anonymous' - img.onload = async () => { + img.onload = () => { try { const width = img.naturalWidth const height = img.naturalHeight // Sample top-right corner (last 25% width, first 25% height) - const color = await fac.getColor(img, { + const color = fac.getColor(img, { left: Math.floor(width * 0.75), top: 0, width: Math.floor(width * 0.25), height: Math.floor(height * 0.25) }) + console.log('Adaptive color detected:', { + hex: color.hex, + rgb: color.rgb, + isLight: color.isLight, + isDark: color.isDark + }) + // Use library's built-in isLight check for optimal contrast if (color.isLight) { + console.log('Light background detected, using black text') setColors({ textColor: '#000000', shadowColor: 'rgba(255, 255, 255, 0.5)' }) } else { + console.log('Dark background detected, using white text') setColors({ textColor: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.5)' From 0c7b11bdf85c134023a920de59b9dab60739d523 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:50:54 +0200 Subject: [PATCH 27/43] fix: improve shadow contrast without background overlay - Increase shadow opacity from 0.5 to 0.8 for better readability - Revert semi-transparent background approach per user feedback - Keep debugging logs to diagnose color detection --- src/hooks/useAdaptiveTextColor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useAdaptiveTextColor.ts b/src/hooks/useAdaptiveTextColor.ts index 75e5564e..55412db0 100644 --- a/src/hooks/useAdaptiveTextColor.ts +++ b/src/hooks/useAdaptiveTextColor.ts @@ -58,13 +58,13 @@ export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveText console.log('Light background detected, using black text') setColors({ textColor: '#000000', - shadowColor: 'rgba(255, 255, 255, 0.5)' + shadowColor: 'rgba(255, 255, 255, 0.8)' }) } else { console.log('Dark background detected, using white text') setColors({ textColor: '#ffffff', - shadowColor: 'rgba(0, 0, 0, 0.5)' + shadowColor: 'rgba(0, 0, 0, 0.8)' }) } } catch (error) { From c9a8a3b91e2205b8b3a6f9fdd1572a426b6a1f28 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:52:54 +0200 Subject: [PATCH 28/43] refactor: remove text shadows from publication date - Remove text-shadow from CSS for .publish-date-topright - Remove shadowColor from useAdaptiveTextColor hook - Only apply adaptive text color, no shadows or backgrounds - Cleaner appearance with color-based readability only --- src/components/ReaderHeader.tsx | 8 +++----- src/hooks/useAdaptiveTextColor.ts | 23 ++++++++--------------- src/styles/components/reader.css | 2 +- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/components/ReaderHeader.tsx b/src/components/ReaderHeader.tsx index 618c5bfb..83edd1c2 100644 --- a/src/components/ReaderHeader.tsx +++ b/src/components/ReaderHeader.tsx @@ -35,7 +35,7 @@ const ReaderHeader: React.FC = ({ highlightVisibility = { nostrverse: true, friends: true, mine: true } }) => { const cachedImage = useImageCache(image) - const { textColor, shadowColor } = useAdaptiveTextColor(cachedImage) + const { textColor } = useAdaptiveTextColor(cachedImage) const formattedDate = published ? format(new Date(published * 1000), 'MMM d, yyyy') : null const isLongSummary = summary && summary.length > 150 @@ -88,8 +88,7 @@ const ReaderHeader: React.FC = ({
{formattedDate} @@ -136,8 +135,7 @@ const ReaderHeader: React.FC = ({
{formattedDate} diff --git a/src/hooks/useAdaptiveTextColor.ts b/src/hooks/useAdaptiveTextColor.ts index 55412db0..69f33e2f 100644 --- a/src/hooks/useAdaptiveTextColor.ts +++ b/src/hooks/useAdaptiveTextColor.ts @@ -3,28 +3,25 @@ import { FastAverageColor } from 'fast-average-color' interface AdaptiveTextColor { textColor: string - shadowColor: string } /** - * Hook to determine optimal text and shadow colors based on image background + * Hook to determine optimal text color based on image background * Samples the top-right corner of the image to ensure publication date is readable * * @param imageUrl - The URL of the image to analyze - * @returns Object containing textColor and shadowColor for optimal contrast + * @returns Object containing textColor for optimal contrast */ export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveTextColor { const [colors, setColors] = useState({ - textColor: '#ffffff', - shadowColor: 'rgba(0, 0, 0, 0.5)' + textColor: '#ffffff' }) useEffect(() => { if (!imageUrl) { // No image, use default white text setColors({ - textColor: '#ffffff', - shadowColor: 'rgba(0, 0, 0, 0.5)' + textColor: '#ffffff' }) return } @@ -57,22 +54,19 @@ export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveText if (color.isLight) { console.log('Light background detected, using black text') setColors({ - textColor: '#000000', - shadowColor: 'rgba(255, 255, 255, 0.8)' + textColor: '#000000' }) } else { console.log('Dark background detected, using white text') setColors({ - textColor: '#ffffff', - shadowColor: 'rgba(0, 0, 0, 0.8)' + textColor: '#ffffff' }) } } catch (error) { // Fallback to default on error console.error('Error analyzing image color:', error) setColors({ - textColor: '#ffffff', - shadowColor: 'rgba(0, 0, 0, 0.5)' + textColor: '#ffffff' }) } } @@ -80,8 +74,7 @@ export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveText img.onerror = () => { // Fallback to default if image fails to load setColors({ - textColor: '#ffffff', - shadowColor: 'rgba(0, 0, 0, 0.5)' + textColor: '#ffffff' }) } diff --git a/src/styles/components/reader.css b/src/styles/components/reader.css index faa9cb27..d8e144b0 100644 --- a/src/styles/components/reader.css +++ b/src/styles/components/reader.css @@ -30,7 +30,7 @@ .reader-meta { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; } .publish-date { display: flex; align-items: center; gap: 0.4rem; font-size: 0.813rem; color: var(--color-text-muted); opacity: 0.85; } .publish-date svg { font-size: 0.75rem; opacity: 0.6; } -.publish-date-topright { position: absolute; top: 1rem; right: 1rem; font-size: 0.813rem; color: var(--color-text); padding: 0.4rem 0.75rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); z-index: 10; } +.publish-date-topright { position: absolute; top: 1rem; right: 1rem; font-size: 0.813rem; color: var(--color-text); padding: 0.4rem 0.75rem; z-index: 10; } .reading-time { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: 6px; font-size: 0.875rem; color: var(--color-text-secondary); } .reading-time svg { font-size: 0.875rem; } .highlight-indicator { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; font-size: 0.875rem; color: var(--color-text); } From aef7b4cea48f1f480c72a001e383fe29c563bc01 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:54:47 +0200 Subject: [PATCH 29/43] fix: include kind:30003 in default bookmark list detection Previously, the dedupeNip51Events function was only looking for kind:10003 and kind:30001 when finding the default bookmark list. This excluded kind:30003 events without a 'd' tag, which is what Primal uses for bookmarks. Now kind:30003 is properly included in the filter. --- src/services/bookmarkEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/bookmarkEvents.ts b/src/services/bookmarkEvents.ts index 88bade3d..a8c8220f 100644 --- a/src/services/bookmarkEvents.ts +++ b/src/services/bookmarkEvents.ts @@ -19,7 +19,7 @@ export function dedupeNip51Events(events: NostrEvent[]): NostrEvent[] { const webBookmarks = unique.filter(e => e.kind === 39701) const bookmarkLists = unique - .filter(e => e.kind === 10003 || e.kind === 30001) + .filter(e => e.kind === 10003 || e.kind === 30003 || e.kind === 30001) .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) const latestBookmarkList = bookmarkLists.find(list => !list.tags?.some((t: string[]) => t[0] === 'd')) From aee9f73316dec380b991235c17fbabd0ad0a44b3 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 15:59:56 +0200 Subject: [PATCH 30/43] debug: add detailed logging for bookmark event tags Added logging to show: - e and a tag counts for all events - which events survived deduplication - specific check for Primal reads list (kind:10003 with d='reads') --- src/services/bookmarkService.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index c9e42963..612c36ea 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -57,11 +57,28 @@ export const fetchBookmarks = async ( rawEvents.forEach((evt, i) => { const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || 'none' const contentPreview = evt.content ? evt.content.slice(0, 50) + (evt.content.length > 50 ? '...' : '') : 'empty' - console.log(` Event ${i}: kind=${evt.kind}, id=${evt.id?.slice(0, 8)}, dTag=${dTag}, contentLength=${evt.content?.length || 0}, contentPreview=${contentPreview}`) + const eTags = evt.tags?.filter((t: string[]) => t[0] === 'e').length || 0 + const aTags = evt.tags?.filter((t: string[]) => t[0] === 'a').length || 0 + console.log(` Event ${i}: kind=${evt.kind}, id=${evt.id?.slice(0, 8)}, dTag=${dTag}, contentLength=${evt.content?.length || 0}, eTags=${eTags}, aTags=${aTags}, contentPreview=${contentPreview}`) }) const bookmarkListEvents = dedupeNip51Events(rawEvents) console.log('📋 After deduplication:', bookmarkListEvents.length, 'bookmark events') + + // Log which events made it through deduplication + bookmarkListEvents.forEach((evt, i) => { + const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || 'none' + console.log(` Dedupe ${i}: kind=${evt.kind}, id=${evt.id?.slice(0, 8)}, dTag="${dTag}"`) + }) + + // Check specifically for Primal's "reads" list + const primalReads = rawEvents.find(e => e.kind === 10003 && e.tags?.find((t: string[]) => t[0] === 'd' && t[1] === 'reads')) + if (primalReads) { + console.log('✅ Found Primal reads list:', primalReads.id.slice(0, 8)) + } else { + console.log('❌ No Primal reads list found (kind:10003 with d="reads")') + } + if (bookmarkListEvents.length === 0) { // Keep existing bookmarks visible; do not clear list if nothing new found return From e6bc4d7fdacd3c3608392def901160d2d1ddd0f9 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:02:30 +0200 Subject: [PATCH 31/43] chore: update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 40037f4e..44af6a48 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist *.log .DS_Store -# Applesauce Reference +# Reference Projects applesauce +primal-web-app From 2193a7a863009ec899510424bf3e5445c6ede453 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:06:03 +0200 Subject: [PATCH 32/43] fix: properly handle AddressPointer bookmarks for long-form articles The issue was that Primal bookmarks long-form articles using 'a' tags (AddressPointer format: kind:pubkey:identifier) but our code was only expecting EventPointer objects with 'id' properties. Changes: - Updated ApplesauceBookmarks interface to match actual applesauce types - Added AddressPointer and EventPointer interfaces - Rewrote processApplesauceBookmarks to handle all bookmark types: * notes (EventPointer) - regular notes * articles (AddressPointer) - long-form content (kind:30023) * hashtags (string[]) * urls (string[]) - Updated bookmark hydration to query addressable events by coordinates - Added logging to show hydration stats This should fix the issue where Primal's Reads bookmarks weren't showing up. --- src/services/bookmarkHelpers.ts | 115 ++++++++++++++++++++++++++------ src/services/bookmarkService.ts | 75 +++++++++++++++++++-- 2 files changed, 164 insertions(+), 26 deletions(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 612bad09..614d5ba4 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -16,11 +16,24 @@ export interface BookmarkData { tags?: string[][] } +export interface AddressPointer { + kind: number + pubkey: string + identifier: string + relays?: string[] +} + +export interface EventPointer { + id: string + relays?: string[] + author?: string +} + export interface ApplesauceBookmarks { - notes?: BookmarkData[] - articles?: BookmarkData[] - hashtags?: BookmarkData[] - urls?: BookmarkData[] + notes?: EventPointer[] + articles?: AddressPointer[] + hashtags?: string[] + urls?: string[] } export interface AccountWithExtension { @@ -55,25 +68,83 @@ export const processApplesauceBookmarks = ( if (typeof bookmarks === 'object' && bookmarks !== null && !Array.isArray(bookmarks)) { const applesauceBookmarks = bookmarks as ApplesauceBookmarks - const allItems: BookmarkData[] = [] - if (applesauceBookmarks.notes) allItems.push(...applesauceBookmarks.notes) - if (applesauceBookmarks.articles) allItems.push(...applesauceBookmarks.articles) - if (applesauceBookmarks.hashtags) allItems.push(...applesauceBookmarks.hashtags) - if (applesauceBookmarks.urls) allItems.push(...applesauceBookmarks.urls) + const allItems: IndividualBookmark[] = [] + + // Process notes (EventPointer[]) + if (applesauceBookmarks.notes) { + applesauceBookmarks.notes.forEach((note: EventPointer) => { + allItems.push({ + id: note.id, + content: '', + created_at: Math.floor(Date.now() / 1000), + pubkey: note.author || activeAccount.pubkey, + kind: 1, // Short note kind + tags: [], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + + // Process articles (AddressPointer[]) + if (applesauceBookmarks.articles) { + applesauceBookmarks.articles.forEach((article: AddressPointer) => { + // Convert AddressPointer to coordinate format: kind:pubkey:identifier + const coordinate = `${article.kind}:${article.pubkey}:${article.identifier || ''}` + allItems.push({ + id: coordinate, + content: '', + created_at: Math.floor(Date.now() / 1000), + pubkey: article.pubkey, + kind: article.kind, // Usually 30023 for long-form articles + tags: [], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + + // Process hashtags (string[]) + if (applesauceBookmarks.hashtags) { + applesauceBookmarks.hashtags.forEach((hashtag: string) => { + allItems.push({ + id: `hashtag-${hashtag}`, + content: `#${hashtag}`, + created_at: Math.floor(Date.now() / 1000), + pubkey: activeAccount.pubkey, + kind: 1, + tags: [['t', hashtag]], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + + // Process URLs (string[]) + if (applesauceBookmarks.urls) { + applesauceBookmarks.urls.forEach((url: string) => { + allItems.push({ + id: `url-${url}`, + content: url, + created_at: Math.floor(Date.now() / 1000), + pubkey: activeAccount.pubkey, + kind: 1, + tags: [['r', url]], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + return allItems - .filter((bookmark: BookmarkData) => bookmark.id) // Skip bookmarks without valid IDs - .map((bookmark: BookmarkData) => ({ - id: bookmark.id!, - content: bookmark.content || '', - created_at: bookmark.created_at || Math.floor(Date.now() / 1000), - pubkey: activeAccount.pubkey, - kind: bookmark.kind || 30001, - tags: bookmark.tags || [], - parsedContent: bookmark.content ? (getParsedContent(bookmark.content) as ParsedContent) : undefined, - type: 'event' as const, - isPrivate, - added_at: bookmark.created_at || Math.floor(Date.now() / 1000) - })) } const bookmarkArray = Array.isArray(bookmarks) ? bookmarks : [bookmarks] diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index 612c36ea..1e5c2f46 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -114,20 +114,87 @@ export const fetchBookmarks = async ( ) const allItems = [...publicItemsAll, ...privateItemsAll] - const noteIds = Array.from(new Set(allItems.map(i => i.id).filter(isHexId))) + + // Separate hex IDs (regular events) from coordinates (addressable events) + const noteIds: string[] = [] + const coordinates: string[] = [] + + allItems.forEach(i => { + if (isHexId(i.id)) { + noteIds.push(i.id) + } else if (i.id.includes(':')) { + // Coordinate format: kind:pubkey:identifier + coordinates.push(i.id) + } + }) + let idToEvent: Map = new Map() + + // Fetch regular events by ID if (noteIds.length > 0) { try { const events = await queryEvents( relayPool, - { ids: noteIds }, + { ids: Array.from(new Set(noteIds)) }, { localTimeoutMs: 800, remoteTimeoutMs: 2500 } ) - idToEvent = new Map(events.map((e: NostrEvent) => [e.id, e])) + events.forEach((e: NostrEvent) => { + idToEvent.set(e.id, e) + // Also store by coordinate if it's an addressable event + if (e.kind && e.kind >= 30000 && e.kind < 40000) { + const dTag = e.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' + const coordinate = `${e.kind}:${e.pubkey}:${dTag}` + idToEvent.set(coordinate, e) + } + }) } catch (error) { - console.warn('Failed to fetch events for hydration:', error) + console.warn('Failed to fetch events by ID:', error) } } + + // Fetch addressable events by coordinates + if (coordinates.length > 0) { + try { + // Group by kind for more efficient querying + const byKind = new Map>() + + coordinates.forEach(coord => { + const parts = coord.split(':') + const kind = parseInt(parts[0]) + const pubkey = parts[1] + const identifier = parts[2] || '' + + if (!byKind.has(kind)) { + byKind.set(kind, []) + } + byKind.get(kind)!.push({ pubkey, identifier }) + }) + + // Query each kind group + for (const [kind, items] of byKind.entries()) { + const authors = Array.from(new Set(items.map(i => i.pubkey))) + const identifiers = Array.from(new Set(items.map(i => i.identifier))) + + const events = await queryEvents( + relayPool, + { kinds: [kind], authors, '#d': identifiers }, + { localTimeoutMs: 800, remoteTimeoutMs: 2500 } + ) + + events.forEach((e: NostrEvent) => { + const dTag = e.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' + const coordinate = `${e.kind}:${e.pubkey}:${dTag}` + idToEvent.set(coordinate, e) + // Also store by event ID + idToEvent.set(e.id, e) + }) + } + } catch (error) { + console.warn('Failed to fetch addressable events:', error) + } + } + + console.log(`📦 Hydration: fetched ${idToEvent.size} events for ${allItems.length} bookmarks (${noteIds.length} notes, ${coordinates.length} articles)`) const allBookmarks = dedupeBookmarksById([ ...hydrateItems(publicItemsAll, idToEvent), ...hydrateItems(privateItemsAll, idToEvent) From 401a8241bdc80d39e82d4e8733553d5a48993b37 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:09:46 +0200 Subject: [PATCH 33/43] fix: resolve lint and type errors - Changed idToEvent from let to const (prefer-const) - Fixed TypeScript type narrowing issue by using direct regex test instead of isHexId type guard - Removed unused isHexId import All lint and type checks now pass for src directory. --- src/services/bookmarkService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index 1e5c2f46..f53b630b 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -5,7 +5,6 @@ import { dedupeNip51Events, hydrateItems, isAccountWithExtension, - isHexId, hasNip04Decrypt, hasNip44Decrypt, dedupeBookmarksById, @@ -120,7 +119,8 @@ export const fetchBookmarks = async ( const coordinates: string[] = [] allItems.forEach(i => { - if (isHexId(i.id)) { + // Check if it's a hex ID (64 character hex string) + if (/^[0-9a-f]{64}$/i.test(i.id)) { noteIds.push(i.id) } else if (i.id.includes(':')) { // Coordinate format: kind:pubkey:identifier @@ -128,7 +128,7 @@ export const fetchBookmarks = async ( } }) - let idToEvent: Map = new Map() + const idToEvent: Map = new Map() // Fetch regular events by ID if (noteIds.length > 0) { From d5a24f0a46cf2ffc4c184898f3bb99d9212b593f Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:11:05 +0200 Subject: [PATCH 34/43] refactor: replace empty state messages with spinners Replaced 'No X yet. Pull to refresh!' messages with spinning loaders for: - No highlights yet (Me & Explore) - No bookmarks yet (Me) - No read articles yet (Me) - No articles written yet (Me) - No blog posts yet (Explore) This provides better UX by showing an active loading state instead of static empty state messages. --- src/components/Explore.tsx | 10 +++++----- src/components/Me.tsx | 24 ++++++++---------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/components/Explore.tsx b/src/components/Explore.tsx index dc2aecd8..e8fe4a2f 100644 --- a/src/components/Explore.tsx +++ b/src/components/Explore.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faNewspaper, faHighlighter, faUser, faUserGroup, faNetworkWired, faArrowsRotate } from '@fortawesome/free-solid-svg-icons' +import { faNewspaper, faHighlighter, faUser, faUserGroup, faNetworkWired, faArrowsRotate, faSpinner } from '@fortawesome/free-solid-svg-icons' import IconButton from './IconButton' import { BlogPostSkeleton, HighlightSkeleton } from './Skeletons' import { Hooks } from 'applesauce-react' @@ -320,8 +320,8 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti ) } return filteredBlogPosts.length === 0 ? ( -
-

No blog posts yet. Pull to refresh!

+
+
) : (
@@ -347,8 +347,8 @@ const Explore: React.FC = ({ relayPool, eventStore, settings, acti ) } return classifiedHighlights.length === 0 ? ( -
-

No highlights yet. Pull to refresh!

+
+
) : (
diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 7ebda014..0d46c25e 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -197,12 +197,8 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ) } return highlights.length === 0 ? ( -
-

- {isOwnProfile - ? 'No highlights yet. Pull to refresh!' - : 'No highlights yet. Pull to refresh!'} -

+
+
) : (
@@ -230,8 +226,8 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ) } return allIndividualBookmarks.length === 0 ? ( -
-

No bookmarks yet. Pull to refresh!

+
+
) : (
@@ -295,8 +291,8 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ) } return readArticles.length === 0 ? ( -
-

No read articles yet. Pull to refresh!

+
+
) : (
@@ -321,12 +317,8 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr ) } return writings.length === 0 ? ( -
-

- {isOwnProfile - ? 'No articles written yet. Pull to refresh!' - : 'No articles written yet. Pull to refresh!'} -

+
+
) : (
From cb3748e06f9a2f597f8eb8fd2367d23ea2490d3a Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:14:04 +0200 Subject: [PATCH 35/43] refactor: remove redundant loading spinner above tabs Removed the loading spinner that appeared above the tab bar since we now show spinners in the empty states themselves, making this redundant. --- src/components/Me.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 0d46c25e..b185a3b1 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -346,12 +346,6 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr
{viewingPubkey && } - {loading && hasData && ( -
- -
- )} -
) } diff --git a/src/components/SidebarHeader.tsx b/src/components/SidebarHeader.tsx index d326eb8f..4997621f 100644 --- a/src/components/SidebarHeader.tsx +++ b/src/components/SidebarHeader.tsx @@ -1,28 +1,22 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faHome, faPlus, faNewspaper, faTimes } from '@fortawesome/free-solid-svg-icons' +import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faHome, faNewspaper, faTimes } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' import { Accounts } from 'applesauce-accounts' -import { RelayPool } from 'applesauce-relay' import IconButton from './IconButton' -import AddBookmarkModal from './AddBookmarkModal' -import { createWebBookmark } from '../services/webBookmarkService' -import { RELAYS } from '../config/relays' interface SidebarHeaderProps { onToggleCollapse: () => void onLogout: () => void onOpenSettings: () => void - relayPool: RelayPool | null isMobile?: boolean } -const SidebarHeader: React.FC = ({ onToggleCollapse, onLogout, onOpenSettings, relayPool, isMobile = false }) => { +const SidebarHeader: React.FC = ({ onToggleCollapse, onLogout, onOpenSettings, isMobile = false }) => { const [isConnecting, setIsConnecting] = useState(false) - const [showAddModal, setShowAddModal] = useState(false) const navigate = useNavigate() const activeAccount = Hooks.useActiveAccount() const accountManager = Hooks.useAccountManager() @@ -54,14 +48,6 @@ const SidebarHeader: React.FC = ({ onToggleCollapse, onLogou return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}` } - const handleSaveBookmark = async (url: string, title?: string, description?: string, tags?: string[]) => { - if (!activeAccount || !relayPool) { - throw new Error('Please login to create bookmarks') - } - - await createWebBookmark(url, title, description, tags, activeAccount, relayPool, RELAYS) - } - const profileImage = getProfileImage() return ( @@ -124,15 +110,6 @@ const SidebarHeader: React.FC = ({ onToggleCollapse, onLogou ariaLabel="Settings" variant="ghost" /> - {activeAccount && ( - setShowAddModal(true)} - title="Add bookmark" - ariaLabel="Add bookmark" - variant="ghost" - /> - )} {activeAccount ? ( = ({ onToggleCollapse, onLogou )}
- {showAddModal && ( - setShowAddModal(false)} - onSave={handleSaveBookmark} - /> - )} ) } From f30c894c87351bb2c71c976787251663092c2e48 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:19:34 +0200 Subject: [PATCH 37/43] fix: align add bookmark button with section heading - Added matching padding to bookmark-section-action button - Button now has same vertical padding as section title (1.5rem top, 0.75rem bottom) - Also handles first section case with reduced padding (0.5rem top) - Removed unnecessary marginBottom from flex container --- src/components/BookmarkList.tsx | 5 +++-- src/styles/components/cards.css | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 9a4d536f..a69ccf0c 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -167,14 +167,15 @@ export const BookmarkList: React.FC = ({ /> {sections.filter(s => s.items.length > 0).map(section => (
-
-

{section.title}

+
+

{section.title}

{section.key === 'web' && activeAccount && ( setShowAddModal(true)} title="Add web bookmark" ariaLabel="Add web bookmark" + className="bookmark-section-action" /> )}
diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index 20726263..20b9ac43 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -21,6 +21,12 @@ border-top: none; padding-top: 0.5rem !important; } +.bookmark-section-action { + padding: 1.5rem 0.5rem 0.75rem; +} +.bookmarks-section:first-of-type .bookmark-section-action { + padding-top: 0.5rem; +} .individual-bookmarks { margin: 1rem 0; } .individual-bookmarks h4 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--color-text); } From 5f3e6335c1cb36f988bc922ad580536b9aa0913d Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:20:06 +0200 Subject: [PATCH 38/43] refactor: reduce section heading bottom padding by half Changed bottom padding from 0.75rem to 0.375rem for both the section title and action button to reduce spacing before bookmark items. --- src/components/BookmarkList.tsx | 2 +- src/styles/components/cards.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index a69ccf0c..60cd3b3d 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -168,7 +168,7 @@ export const BookmarkList: React.FC = ({ {sections.filter(s => s.items.length > 0).map(section => (
-

{section.title}

+

{section.title}

{section.key === 'web' && activeAccount && ( Date: Wed, 15 Oct 2025 16:20:35 +0200 Subject: [PATCH 39/43] refactor: make section dividers more subtle Changed border color from var(--color-border) to rgba(255, 255, 255, 0.05) for a much more subtle dividing line between bookmark sections. --- src/styles/components/cards.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index a3a0c3be..26fb62d2 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -15,7 +15,7 @@ color: var(--color-text-muted) !important; padding: 1.5rem 0.5rem 0.375rem !important; margin: 0 !important; - border-top: 1px solid var(--color-border); + border-top: 1px solid rgba(255, 255, 255, 0.05); } .bookmarks-section:first-of-type .bookmarks-section-title { border-top: none; From b19f5f55f706acd2593aff61a2dcbda29d9ca3ba Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:21:26 +0200 Subject: [PATCH 40/43] fix: remove borders from compact bookmark cards --- src/styles/components/cards.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css index 26fb62d2..57bc563f 100644 --- a/src/styles/components/cards.css +++ b/src/styles/components/cards.css @@ -44,8 +44,8 @@ .individual-bookmark:hover { border-color: var(--color-border); background: var(--color-bg-elevated); } /* Compact view */ -.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none; border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; } -.individual-bookmark.compact:hover { background: var(--color-bg-elevated); transform: none; box-shadow: none; } +.individual-bookmark.compact { padding: 0.5rem 0.5rem; background: transparent; border: none !important; border-radius: 0; box-shadow: none; width: 100%; max-width: 100%; overflow: hidden; } +.individual-bookmark.compact:hover { background: var(--color-bg-elevated); transform: none; box-shadow: none; border: none !important; } .compact-row { display: flex; align-items: center; gap: 0.5rem; height: 28px; width: 100%; min-width: 0; overflow: hidden; } .compact-thumbnail { width: 24px; height: 24px; flex-shrink: 0; border-radius: 4px; overflow: hidden; background: var(--color-bg-elevated); display: flex; align-items: center; justify-content: center; } .compact-thumbnail img { width: 100%; height: 100%; object-fit: cover; } From 54bd59fa2deb57fb73851e2e781207f044cb5a93 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:25:03 +0200 Subject: [PATCH 41/43] refactor: rename Amethyst-style bookmarks to Old Bookmarks (Legacy) --- src/components/BookmarkList.tsx | 2 +- src/components/Me.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 60cd3b3d..13343259 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -97,7 +97,7 @@ export const BookmarkList: React.FC = ({ { key: 'private', title: 'Private bookmarks', items: groups.privateItems }, { key: 'public', title: 'Public bookmarks', items: groups.publicItems }, { key: 'web', title: 'Web bookmarks', items: groups.web }, - { key: 'amethyst', title: 'Amethyst-style bookmarks', items: groups.amethyst } + { key: 'amethyst', title: 'Old Bookmarks (Legacy)', items: groups.amethyst } ] // Add bookmark sets as additional sections diff --git a/src/components/Me.tsx b/src/components/Me.tsx index b185a3b1..64bc4e6c 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -177,7 +177,7 @@ const Me: React.FC = ({ relayPool, activeTab: propActiveTab, pubkey: pr { key: 'private', title: 'Private bookmarks', items: groups.privateItems }, { key: 'public', title: 'Public bookmarks', items: groups.publicItems }, { key: 'web', title: 'Web bookmarks', items: groups.web }, - { key: 'amethyst', title: 'Amethyst-style bookmarks', items: groups.amethyst } + { key: 'amethyst', title: 'Old Bookmarks (Legacy)', items: groups.amethyst } ] // Show content progressively - no blocking error screens From ec8584b4d2dc6b7443e4f034c036244e066a9828 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:25:48 +0200 Subject: [PATCH 42/43] feat: hide cover images in compact view --- src/components/BookmarkViews/CompactView.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index d7abf29e..93c186d3 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -5,7 +5,6 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core' import { IndividualBookmark } from '../../types/bookmarks' import { formatDateCompact } from '../../utils/bookmarkUtils' import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' -import { useImageCache } from '../../hooks/useImageCache' interface CompactViewProps { bookmark: IndividualBookmark @@ -32,9 +31,6 @@ export const CompactView: React.FC = ({ const isWebBookmark = bookmark.kind === 39701 const isClickable = hasUrls || isArticle || isWebBookmark - // Get cached image for thumbnail - const cachedImage = useImageCache(articleImage || undefined) - const handleCompactClick = () => { if (!onSelectUrl) return @@ -58,13 +54,6 @@ export const CompactView: React.FC = ({ role={isClickable ? 'button' : undefined} tabIndex={isClickable ? 0 : undefined} > - {/* Thumbnail image */} - {cachedImage && ( -
- -
- )} - {bookmark.isPrivate && ( From 8bccc9de4842e2b27389b429468a4a6f9a0b77c0 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 15 Oct 2025 16:27:05 +0200 Subject: [PATCH 43/43] fix: remove unused articleImage prop from CompactView --- src/components/BookmarkItem.tsx | 3 ++- src/components/BookmarkViews/CompactView.tsx | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 381678aa..f842879a 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -142,7 +142,8 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS } if (viewMode === 'compact') { - return + const { articleImage: _articleImage, ...compactProps } = sharedProps + return } if (viewMode === 'large') { diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index 93c186d3..46e06240 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -12,7 +12,6 @@ interface CompactViewProps { hasUrls: boolean extractedUrls: string[] onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void - articleImage?: string articleSummary?: string contentTypeIcon: IconDefinition } @@ -23,7 +22,6 @@ export const CompactView: React.FC = ({ hasUrls, extractedUrls, onSelectUrl, - articleImage, articleSummary, contentTypeIcon }) => {