Compare commits

...

16 Commits

Author SHA1 Message Date
Gigi
8da0a06711 chore: bump version to 0.6.20 2025-10-15 21:53:06 +02:00
Gigi
be8d857223 Merge pull request #12 from dergigi/bookmark-filter-buttons
Add bookmark filter buttons by content type
2025-10-15 21:52:37 +02:00
Gigi
d50bcd700e fix(ui): make highlight button fixed to viewport 2025-10-15 21:51:24 +02:00
Gigi
820ab1d902 fix(ui): make highlight button sticky and always visible
- Wrap button in sticky positioned container with height: 0
- Button now floats and stays visible while scrolling
- Remains within reader pane boundaries on desktop
- Uses flexbox to align button to the right side
2025-10-15 21:48:41 +02:00
Gigi
f5e9e5bf61 fix(ui): position highlight button inside reader pane
- Move HighlightButton from fixed viewport positioning to absolute positioning within main pane
- Add position: relative to .pane.main for both desktop and mobile layouts
- Button now stays within the article/reader view instead of floating outside on desktop
- Maintains proper z-index and responsive behavior
2025-10-15 21:47:28 +02:00
Gigi
40b43532e8 style: use faLink icon for external articles
- Replace faArrowUpRightFromSquare with simpler faLink icon
- More concise visual representation for external article links
2025-10-15 21:40:31 +02:00
Gigi
51a3008730 feat: add separate filter for external articles with distinct icon
- Add 'external' type to differentiate external article links from nostr-native articles
- Nostr-native articles (kind:30023) use newspaper icon
- External article links use arrow-up-right icon (faArrowUpRightFromSquare)
- Add new 'External Articles' filter button
- Update classification logic and icon display accordingly
2025-10-15 21:39:10 +02:00
Gigi
e30cbc72c3 style: dramatically reduce whitespace around bookmark filters
- Remove all padding from filter buttons
- Reduce top padding from 0.75rem to 0.25rem
- Reduce bottom margin from 0.5rem to 0.25rem
- Much tighter, more compact layout
2025-10-15 21:35:44 +02:00
Gigi
6f913262f4 style: reduce whitespace around bookmark filters on /me page
- Reduce padding on bookmark filters from 1rem to 0.5rem
- Reduce top padding of tab content when filters are present
- Tighten spacing for more compact layout
2025-10-15 21:35:11 +02:00
Gigi
0f0462e6ac feat: add bookmark filters to /me page bookmarks tab
- Add filter buttons to reading-list tab in Me component
- Apply same filtering logic as main bookmarks sidebar
- Center-align filters and remove border for cleaner look
- Show empty state message when no bookmarks match filter
2025-10-15 21:24:19 +02:00
Gigi
e353f0e2d6 style: refine bookmark filter buttons
- Make buttons smaller (32px) and more compact
- Remove borders for cleaner look
- Active state uses primary color without background
- Match icon styling used on bookmark cards
2025-10-15 21:19:16 +02:00
Gigi
ee1365d3ca feat: add bookmark filter buttons by content type
- Add BookmarkFilters component with icon-based filter buttons
- Create bookmarkTypeClassifier utility for content type classification
- Filter bookmarks by article, video, note, or web types
- Apply filters across all bookmark lists (private, public, web, sets)
- Style filter buttons to match existing UI design
2025-10-15 21:17:27 +02:00
Gigi
a215d0b026 refactor: remove lock icon from individual bookmarks
- Private bookmarks are now grouped in 'Private Bookmarks' section
- No need for redundant lock icon on each individual bookmark
- Cleaner UI with less visual clutter
- Removed faUserLock import and conditional rendering from all three views
2025-10-15 20:37:40 +02:00
Gigi
b8d76c0bd8 feat: move encrypted legacy bookmarks to Private Bookmarks section
- Only non-encrypted legacy bookmarks (kind:30001) now appear in Legacy section
- Encrypted legacy bookmarks are grouped with other private bookmarks
- Improves organization by grouping by privacy level rather than source
2025-10-15 20:36:00 +02:00
Gigi
233169b082 feat: improve bookmark section labels for clarity
- Capitalize all bookmark section labels for consistency
- Change 'Old Bookmarks (Legacy)' to 'Legacy Bookmarks' for cleaner look
- Updated labels in both BookmarkList and Me components
2025-10-15 20:35:19 +02:00
Gigi
72b9a04cd2 docs: update CHANGELOG.md for v0.6.19 2025-10-15 20:01:43 +02:00
13 changed files with 207 additions and 33 deletions

View File

@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.6.19] - 2025-10-15
### Fixed
- Highlights disappearing on external URLs after a few seconds
- Fixed `useBookmarksData` from fetching general highlights when viewing external URLs
- External URL highlights now managed exclusively by `useExternalUrlLoader`
- Removed redundant `setHighlights` call that was overwriting streamed highlights
- Improved error handling in `fetchHighlightsForUrl` to prevent silent failures
- Isolated rebroadcast errors so they don't break highlight display
- Added logging to help diagnose highlight fetching issues
## [0.6.18] - 2025-10-15
### Changed
@@ -1595,7 +1607,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimize relay usage following applesauce-relay best practices
- Use applesauce-react event models for better profile handling
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.18...HEAD
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.19...HEAD
[0.6.19]: https://github.com/dergigi/boris/compare/v0.6.18...v0.6.19
[0.6.18]: https://github.com/dergigi/boris/compare/v0.6.17...v0.6.18
[0.6.17]: https://github.com/dergigi/boris/compare/v0.6.16...v0.6.17
[0.6.16]: https://github.com/dergigi/boris/compare/v0.6.15...v0.6.16

View File

@@ -1,6 +1,6 @@
{
"name": "boris",
"version": "0.6.19",
"version": "0.6.20",
"description": "A minimal nostr client for bookmark management",
"homepage": "https://read.withboris.com/",
"type": "module",

View File

@@ -0,0 +1,44 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faNewspaper, faStickyNote, faCirclePlay } from '@fortawesome/free-regular-svg-icons'
import { faGlobe, faAsterisk, faLink } from '@fortawesome/free-solid-svg-icons'
export type BookmarkFilterType = 'all' | 'article' | 'external' | 'video' | 'note' | 'web'
interface BookmarkFiltersProps {
selectedFilter: BookmarkFilterType
onFilterChange: (filter: BookmarkFilterType) => void
}
const BookmarkFilters: React.FC<BookmarkFiltersProps> = ({
selectedFilter,
onFilterChange
}) => {
const filters = [
{ type: 'all' as const, icon: faAsterisk, label: 'All' },
{ type: 'article' as const, icon: faNewspaper, label: 'Articles' },
{ type: 'external' as const, icon: faLink, label: 'External Articles' },
{ type: 'video' as const, icon: faCirclePlay, label: 'Videos' },
{ type: 'note' as const, icon: faStickyNote, label: 'Notes' },
{ type: 'web' as const, icon: faGlobe, label: 'Web' }
]
return (
<div className="bookmark-filters">
{filters.map(filter => (
<button
key={filter.type}
onClick={() => onFilterChange(filter.type)}
className={`filter-btn ${selectedFilter === filter.type ? 'active' : ''}`}
title={filter.label}
aria-label={`Filter by ${filter.label}`}
>
<FontAwesomeIcon icon={filter.icon} />
</button>
))}
</div>
)
}
export default BookmarkFilters

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { faNewspaper, faStickyNote, faCirclePlay, faCamera, faFileLines } from '@fortawesome/free-regular-svg-icons'
import { faGlobe } from '@fortawesome/free-solid-svg-icons'
import { faGlobe, faLink } from '@fortawesome/free-solid-svg-icons'
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { useEventModel } from 'applesauce-react/hooks'
import { Models } from 'applesauce-core'
@@ -70,7 +70,7 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
// Get content type icon based on bookmark kind and URL classification
const getContentTypeIcon = (): IconDefinition => {
if (isArticle) return faNewspaper
if (isArticle) return faNewspaper // Nostr-native article
// For web bookmarks, classify the URL to determine icon
if (isWebBookmark && firstUrlClassification) {
@@ -81,7 +81,7 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
case 'image':
return faCamera
case 'article':
return faNewspaper
return faLink // External article
default:
return faGlobe
}
@@ -89,6 +89,7 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
if (!hasUrls) return faStickyNote // Just a text note
if (firstUrlClassification?.type === 'youtube' || firstUrlClassification?.type === 'video') return faCirclePlay
if (firstUrlClassification?.type === 'article') return faLink // External article
return faFileLines
}

View File

@@ -19,6 +19,8 @@ import AddBookmarkModal from './AddBookmarkModal'
import { createWebBookmark } from '../services/webBookmarkService'
import { RELAYS } from '../config/relays'
import { Hooks } from 'applesauce-react'
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
interface BookmarkListProps {
bookmarks: Bookmark[]
@@ -61,6 +63,7 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
const bookmarksListRef = useRef<HTMLDivElement>(null)
const friendsColor = settings?.highlightColorFriends || '#f97316'
const [showAddModal, setShowAddModal] = useState(false)
const [selectedFilter, setSelectedFilter] = useState<BookmarkFilterType>('all')
const activeAccount = Hooks.useActiveAccount()
const handleSaveBookmark = async (url: string, title?: string, description?: string, tags?: string[]) => {
@@ -87,17 +90,20 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
.filter(hasContent)
// Apply filter
const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, selectedFilter)
// Separate bookmarks with setName (kind 30003) from regular bookmarks
const bookmarksWithoutSet = getBookmarksWithoutSet(allIndividualBookmarks)
const bookmarkSets = getBookmarkSets(allIndividualBookmarks)
const bookmarksWithoutSet = getBookmarksWithoutSet(filteredBookmarks)
const bookmarkSets = getBookmarkSets(filteredBookmarks)
// 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 },
{ key: 'web', title: 'Web bookmarks', items: groups.web },
{ key: 'amethyst', title: 'Old Bookmarks (Legacy)', 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: 'Legacy Bookmarks', items: groups.amethyst }
]
// Add bookmark sets as additional sections
@@ -140,7 +146,18 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
isMobile={isMobile}
/>
{allIndividualBookmarks.length === 0 ? (
{allIndividualBookmarks.length > 0 && (
<BookmarkFilters
selectedFilter={selectedFilter}
onFilterChange={setSelectedFilter}
/>
)}
{filteredBookmarks.length === 0 && allIndividualBookmarks.length > 0 ? (
<div className="empty-state">
<p>No bookmarks match this filter.</p>
</div>
) : allIndividualBookmarks.length === 0 ? (
loading ? (
<div className={`bookmarks-list ${viewMode}`} aria-busy="true">
<div className={`bookmarks-grid bookmarks-${viewMode}`}>

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUserLock, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { 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'
@@ -91,9 +91,6 @@ export const CardView: React.FC<CardViewProps> = ({
<div className="bookmark-header">
<span className="bookmark-type">
<FontAwesomeIcon icon={contentTypeIcon} className="content-type-icon" />
{bookmark.isPrivate && (
<FontAwesomeIcon icon={faUserLock} className="bookmark-visibility private" />
)}
</span>
{eventNevent ? (

View File

@@ -1,6 +1,5 @@
import React from 'react'
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 { formatDateCompact } from '../../utils/bookmarkUtils'
@@ -54,9 +53,6 @@ export const CompactView: React.FC<CompactViewProps> = ({
>
<span className="bookmark-type-compact">
<FontAwesomeIcon icon={contentTypeIcon} className="content-type-icon" />
{bookmark.isPrivate && (
<FontAwesomeIcon icon={faUserLock} className="bookmark-visibility private" />
)}
</span>
{displayText && (
<div className="compact-text">

View File

@@ -1,7 +1,6 @@
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'
@@ -96,9 +95,6 @@ export const LargeView: React.FC<LargeViewProps> = ({
<div className="large-footer">
<span className="bookmark-type-large">
<FontAwesomeIcon icon={contentTypeIcon} className="content-type-icon" />
{bookmark.isPrivate && (
<FontAwesomeIcon icon={faUserLock} className="bookmark-visibility private" />
)}
</span>
<span className="large-author">
<Link

View File

@@ -24,6 +24,8 @@ import { faBooks } from '../icons/customIcons'
import { usePullToRefresh } from 'use-pull-to-refresh'
import RefreshIndicator from './RefreshIndicator'
import { groupIndividualBookmarks, hasContent } from '../utils/bookmarkUtils'
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
interface MeProps {
relayPool: RelayPool
@@ -48,6 +50,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
const [loading, setLoading] = useState(true)
const [viewMode, setViewMode] = useState<ViewMode>('cards')
const [refreshTrigger, setRefreshTrigger] = useState(0)
const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all')
// Update local state when prop changes
useEffect(() => {
@@ -172,12 +175,16 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
// Merge and flatten all individual bookmarks
const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
.filter(hasContent)
const groups = groupIndividualBookmarks(allIndividualBookmarks)
// Apply filter
const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, bookmarkFilter)
const groups = groupIndividualBookmarks(filteredBookmarks)
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: 'Old Bookmarks (Legacy)', 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: 'Legacy Bookmarks', items: groups.amethyst }
]
// Show content progressively - no blocking error screens
@@ -231,7 +238,18 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
</div>
) : (
<div className="bookmarks-list">
{sections.filter(s => s.items.length > 0).map(section => (
{allIndividualBookmarks.length > 0 && (
<BookmarkFilters
selectedFilter={bookmarkFilter}
onFilterChange={setBookmarkFilter}
/>
)}
{filteredBookmarks.length === 0 ? (
<div className="explore-loading" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '4rem', color: 'var(--text-secondary)' }}>
No bookmarks match this filter.
</div>
) : (
sections.filter(s => s.items.length > 0).map(section => (
<div key={section.key} className="bookmarks-section">
<h3 className="bookmarks-section-title">{section.title}</h3>
<div className={`bookmarks-grid bookmarks-${viewMode}`}>
@@ -246,7 +264,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
))}
</div>
</div>
))}
)))}
<div className="view-mode-controls" style={{
display: 'flex',
justifyContent: 'center',

View File

@@ -67,6 +67,10 @@
width: 100%;
}
.me-tab-content:has(.bookmark-filters) {
padding-top: 0.25rem;
}
/* Align highlight list width with profile card width on /me */
.me-highlights-list { padding-left: 0; padding-right: 0; }
.explore-header .author-card { max-width: 600px; margin: 0 auto; width: 100%; }
@@ -79,6 +83,15 @@
text-align: left; /* Override center alignment from .app */
}
/* Bookmark filters in Me page */
.me-tab-content .bookmark-filters {
background: transparent;
border: none;
padding: 0;
justify-content: center;
margin-bottom: 0.25rem;
}
/* Ensure all reading list elements are left-aligned */
.bookmarks-list .individual-bookmark,
.bookmarks-list .individual-bookmark * {

View File

@@ -176,3 +176,38 @@
.read-inline-btn { background: rgb(34 197 94); /* green-500 */ color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; }
.read-inline-btn:hover { background: rgb(22 163 74); /* green-600 */ }
/* Bookmark filters */
.bookmark-filters {
display: flex;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg);
}
.bookmark-filters .filter-btn {
background: transparent;
color: var(--color-text-secondary);
border: none;
padding: 0.375rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
min-width: 32px;
min-height: 32px;
}
.bookmark-filters .filter-btn:hover {
color: var(--color-text);
background: var(--color-bg-elevated);
}
.bookmark-filters .filter-btn.active {
color: var(--color-primary);
background: transparent;
}

View File

@@ -0,0 +1,42 @@
import { IndividualBookmark } from '../types/bookmarks'
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
import { classifyUrl } from './helpers'
export type BookmarkType = 'article' | 'external' | 'video' | 'note' | 'web'
/**
* Classifies a bookmark into one of the content types
*/
export function classifyBookmarkType(bookmark: IndividualBookmark): BookmarkType {
// Kind 30023 is always a nostr-native article
if (bookmark.kind === 30023) return 'article'
const isWebBookmark = bookmark.kind === 39701
const webBookmarkUrl = isWebBookmark ? bookmark.tags.find(t => t[0] === 'd')?.[1] : null
const extractedUrls = webBookmarkUrl
? [webBookmarkUrl.startsWith('http') ? webBookmarkUrl : `https://${webBookmarkUrl}`]
: extractUrlsFromContent(bookmark.content)
const firstUrl = extractedUrls[0]
if (!firstUrl) return 'note'
const urlType = classifyUrl(firstUrl)?.type
if (urlType === 'youtube' || urlType === 'video') return 'video'
if (urlType === 'article') return 'external' // External article links
return 'web'
}
/**
* Filters bookmarks by type
*/
export function filterBookmarksByType(
bookmarks: IndividualBookmark[],
filterType: 'all' | BookmarkType
): IndividualBookmark[] {
if (filterType === 'all') return bookmarks
return bookmarks.filter(bookmark => classifyBookmarkType(bookmark) === filterType)
}

View File

@@ -92,10 +92,12 @@ export const sortIndividualBookmarks = (items: IndividualBookmark[]) => {
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')
// 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)
const privateItems = sorted.filter(i => i.isPrivate && !isIn(amethyst, i) && !isIn(web, i))
// 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 }
}