From 4da3a0347f3541d636e10ac10b6c58b876e1215e Mon Sep 17 00:00:00 2001 From: Gigi Date: Fri, 17 Oct 2025 23:55:15 +0200 Subject: [PATCH] feat: add bookmark grouping toggle (grouped by source vs flat chronological) Changes: - Updated groupIndividualBookmarks to group by source kind (10003, 30001, 39701) instead of content type - Added toggle button in bookmark footer to switch between grouped and flat views - Default mode is 'grouped by source' showing: My Bookmarks, Private Bookmarks, Amethyst Lists, Web Bookmarks - Flat mode shows single 'All Bookmarks (X)' section sorted chronologically - Preference persists to localStorage - Implemented in both BookmarkList.tsx and Me.tsx Files modified: - src/utils/bookmarkUtils.tsx - New grouping logic - src/components/BookmarkList.tsx - Added state, toggle button, conditional sections - src/components/Me.tsx - Added state, toggle button, conditional sections --- src/components/BookmarkList.tsx | 37 ++++++++++++++++++++++++++------- src/components/Me.tsx | 35 ++++++++++++++++++++++++------- src/utils/bookmarkUtils.tsx | 23 +++++++++++++------- 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index eb920eab..c0ab704c 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -1,7 +1,7 @@ import React, { useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faChevronLeft, faBookmark, faList, faThLarge, faImage, faRotate, faHeart, faPlus } from '@fortawesome/free-solid-svg-icons' +import { faChevronLeft, faBookmark, faList, faThLarge, faImage, faRotate, faHeart, faPlus, faLayerGroup, faBars } from '@fortawesome/free-solid-svg-icons' import { formatDistanceToNow } from 'date-fns' import { RelayPool } from 'applesauce-relay' import { Bookmark, IndividualBookmark } from '../types/bookmarks' @@ -65,8 +65,18 @@ export const BookmarkList: React.FC = ({ const friendsColor = settings?.highlightColorFriends || '#f97316' const [showAddModal, setShowAddModal] = useState(false) const [selectedFilter, setSelectedFilter] = useState('all') + const [groupingMode, setGroupingMode] = useState<'grouped' | 'flat'>(() => { + const saved = localStorage.getItem('bookmarkGroupingMode') + return saved === 'flat' ? 'flat' : 'grouped' + }) const activeAccount = Hooks.useActiveAccount() + const toggleGroupingMode = () => { + const newMode = groupingMode === 'grouped' ? 'flat' : 'grouped' + setGroupingMode(newMode) + localStorage.setItem('bookmarkGroupingMode', newMode) + } + const handleSaveBookmark = async (url: string, title?: string, description?: string, tags?: string[]) => { if (!activeAccount || !relayPool) { throw new Error('Please login to create bookmarks') @@ -98,14 +108,18 @@ export const BookmarkList: React.FC = ({ const bookmarksWithoutSet = getBookmarksWithoutSet(filteredBookmarks) const bookmarkSets = getBookmarkSets(filteredBookmarks) - // Group non-set bookmarks as before + // Group non-set bookmarks by source or flatten based on mode 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 }, - { key: 'web', title: 'Web Bookmarks', items: groups.web }, - { key: 'amethyst', title: 'Legacy Bookmarks', items: groups.amethyst } - ] + const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = + groupingMode === 'flat' + ? [{ key: 'all', title: `All Bookmarks (${bookmarksWithoutSet.length})`, items: bookmarksWithoutSet }] + : [ + { key: 'nip51-private', title: 'Private Bookmarks', items: groups.nip51Private }, + { key: 'nip51-public', title: 'My Bookmarks', items: groups.nip51Public }, + { key: 'amethyst-private', title: 'Amethyst Private', items: groups.amethystPrivate }, + { key: 'amethyst-public', title: 'Amethyst Lists', items: groups.amethystPublic }, + { key: 'web', title: 'Web Bookmarks', items: groups.standaloneWeb } + ] // Add bookmark sets as additional sections bookmarkSets.forEach(set => { @@ -236,6 +250,13 @@ export const BookmarkList: React.FC = ({ spin={isRefreshing} /> )} + onViewModeChange('compact')} diff --git a/src/components/Me.tsx b/src/components/Me.tsx index 6f6c6654..0e74dd74 100644 --- a/src/components/Me.tsx +++ b/src/components/Me.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare, faLink } from '@fortawesome/free-solid-svg-icons' +import { faHighlighter, faBookmark, faList, faThLarge, faImage, faPenToSquare, faLink, faLayerGroup, faBars } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { BlogPostSkeleton, HighlightSkeleton, BookmarkSkeleton } from './Skeletons' import { RelayPool } from 'applesauce-relay' @@ -70,6 +70,16 @@ const Me: React.FC = ({ const [viewMode, setViewMode] = useState('cards') const [refreshTrigger, setRefreshTrigger] = useState(0) const [bookmarkFilter, setBookmarkFilter] = useState('all') + const [groupingMode, setGroupingMode] = useState<'grouped' | 'flat'>(() => { + const saved = localStorage.getItem('bookmarkGroupingMode') + return saved === 'flat' ? 'flat' : 'grouped' + }) + + const toggleGroupingMode = () => { + const newMode = groupingMode === 'grouped' ? 'flat' : 'grouped' + setGroupingMode(newMode) + localStorage.setItem('bookmarkGroupingMode', newMode) + } // Initialize reading progress filter from URL param const initialFilter = urlFilter && VALID_FILTERS.includes(urlFilter as ReadingProgressFilterType) @@ -391,12 +401,16 @@ const Me: React.FC = ({ // Apply reading progress filter const filteredReads = filterByReadingProgress(reads, readingProgressFilter) const filteredLinks = filterByReadingProgress(links, readingProgressFilter) - 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 }, - { key: 'web', title: 'Web Bookmarks', items: groups.web }, - { key: 'amethyst', title: 'Legacy Bookmarks', items: groups.amethyst } - ] + const sections: Array<{ key: string; title: string; items: IndividualBookmark[] }> = + groupingMode === 'flat' + ? [{ key: 'all', title: `All Bookmarks (${filteredBookmarks.length})`, items: filteredBookmarks }] + : [ + { key: 'nip51-private', title: 'Private Bookmarks', items: groups.nip51Private }, + { key: 'nip51-public', title: 'My Bookmarks', items: groups.nip51Public }, + { key: 'amethyst-private', title: 'Amethyst Private', items: groups.amethystPrivate }, + { key: 'amethyst-public', title: 'Amethyst Lists', items: groups.amethystPublic }, + { key: 'web', title: 'Web Bookmarks', items: groups.standaloneWeb } + ] // Show content progressively - no blocking error screens const hasData = highlights.length > 0 || bookmarks.length > 0 || reads.length > 0 || links.length > 0 || writings.length > 0 @@ -484,6 +498,13 @@ const Me: React.FC = ({ marginTop: '1rem', borderTop: '1px solid var(--border-color)' }}> + setViewMode('compact')} diff --git a/src/utils/bookmarkUtils.tsx b/src/utils/bookmarkUtils.tsx index 6fe2dcff..cf96cc93 100644 --- a/src/utils/bookmarkUtils.tsx +++ b/src/utils/bookmarkUtils.tsx @@ -92,14 +92,21 @@ export const sortIndividualBookmarks = (items: IndividualBookmark[]) => { export function groupIndividualBookmarks(items: IndividualBookmark[]) { const sorted = sortIndividualBookmarks(items) - const web = sorted.filter(i => i.kind === 39701 || i.type === 'web') - // Only non-encrypted legacy bookmarks go to the amethyst section - const amethyst = sorted.filter(i => i.sourceKind === 30001 && !i.isPrivate) - const isIn = (list: IndividualBookmark[], x: IndividualBookmark) => list.some(i => i.id === x.id) - // Private items include encrypted legacy bookmarks - const privateItems = sorted.filter(i => i.isPrivate && !isIn(web, i)) - const publicItems = sorted.filter(i => !i.isPrivate && !isIn(amethyst, i) && !isIn(web, i)) - return { privateItems, publicItems, web, amethyst } + + // Group by source list, not by content type + const nip51Public = sorted.filter(i => i.sourceKind === 10003 && !i.isPrivate) + const nip51Private = sorted.filter(i => i.sourceKind === 10003 && i.isPrivate) + const amethystPublic = sorted.filter(i => i.sourceKind === 30001 && !i.isPrivate) + const amethystPrivate = sorted.filter(i => i.sourceKind === 30001 && i.isPrivate) + const standaloneWeb = sorted.filter(i => i.sourceKind === 39701) + + return { + nip51Public, + nip51Private, + amethystPublic, + amethystPrivate, + standaloneWeb + } } // Simple filter: only exclude bookmarks with empty/whitespace-only content