Compare commits

..

22 Commits

Author SHA1 Message Date
Gigi
84001d1b83 chore: bump version to 0.10.22 2025-10-25 00:10:49 +02:00
Gigi
b7a390cf89 Merge pull request #25 from dergigi/trying-to-fix-annoying-bugs
Mobile UX improvements and bug fixes
2025-10-25 00:10:31 +02:00
Gigi
59d9179642 ui: hide tab text labels on mobile for /my and /p/ pages
- Add CSS media query to hide .tab-label on screens ≤768px
- Adjust tab padding and gap for mobile to show only icons
- Saves space on mobile for highlights, bookmarks, reads, links, writings tabs
- Affects Me, Profile, and Explore components
2025-10-25 00:08:40 +02:00
Gigi
68301cd20f brand: update tagline from 'Nostr Bookmarks' to 'Read, Highlight, Explore'
- Update HTML title and meta tags in index.html
- Update PWA manifest in public/manifest.webmanifest
- Update Vite PWA manifest in vite.config.ts
- Update article OG title fallback in api/article-og.ts
- Reflects the core functionality: reading, highlighting, and exploring content
2025-10-25 00:06:19 +02:00
Gigi
4d6b7e1a46 ui: reorder bookmarks bar buttons
- Change button order to: Home, Explore, Settings
- Maintains all existing functionality
2025-10-25 00:05:23 +02:00
Gigi
95fe9b548f fix: close mobile sidebar when clicking navigation items
- Add mobile sidebar close logic to handleMenuItemClick for profile menu items
- Add mobile sidebar close logic to Home, Settings, and Explore buttons
- Fixes issue where mobile bookmarks bar didn't close when navigating to My Reads, My Highlights, etc.
2025-10-25 00:05:04 +02:00
Gigi
e86ae9f05e ui: move highlight button higher up
- Change bottom position from 32px to 80px
- Provides more space from bottom edge for better positioning
2025-10-25 00:04:02 +02:00
Gigi
2124be83c3 refactor: remove versioning from reading progress implementation
- Remove 'ver' field from ReadingProgressContent interface
- Remove ver: '1' from saveReadingPosition function
- Update PROGRESS_CACHE_KEY to remove v1 suffix
2025-10-25 00:02:40 +02:00
Gigi
a8bb17d4cd fix: replace any types with proper type-safe inert attribute handling 2025-10-23 22:06:35 +02:00
Gigi
a886a68822 feat(highlights): increase fetch limits for better explore page coverage
- Increase friends highlights limit from 200 to 1000
- Add 1000 limit to nostrverse highlights on initial load
- Ensures users see more highlights from friends and nostrverse
- Incremental syncs continue to use 'since' filter without limit
2025-10-23 21:59:58 +02:00
Gigi
76bdbc670d fix(nostrverse): merge incremental highlights instead of replacing
- Initialize highlightsMap with existing highlights on incremental sync
- Merge new highlights with existing ones instead of replacing entire list
- Keep existing highlights on error instead of clearing them
- Fixes issue where nostrverse highlights would disappear on page refresh
2025-10-23 21:54:53 +02:00
Gigi
c16ce1fc7e feat(explore): persist scope visibility to localStorage
- Load explore scope visibility from localStorage on mount
- Save user's scope toggles to localStorage when changed
- Only reset to settings defaults if no saved preference exists
- Ensures user's scope selection persists across page refreshes
2025-10-23 21:43:48 +02:00
Gigi
a578d67b1e fix: load reading positions for web bookmarks (kind:39701)
Web bookmarks store their URL in the 'd' tag, not in content.
The getBookmarkReadingProgress function was only extracting URLs from
content, which meant reading progress indicators weren't showing for
web bookmarks. Now it properly extracts URLs from the 'd' tag for
kind:39701 bookmarks.
2025-10-23 21:14:15 +02:00
Gigi
25d1ead9f5 fix: add error handling when closing relay connections
Wrap relay.close() in try-catch to gracefully handle cases where WebSocket
connections are closed before they're fully established. This can occur when
relay sets change rapidly during app initialization (e.g., when loading user
relay lists).
2025-10-23 20:52:42 +02:00
Gigi
ae5ea66dd2 fix(a11y): replace aria-hidden with inert to prevent focus issues
Replaced aria-hidden with inert attribute on mobile sidebar and highlights panes. The inert attribute both hides from assistive technology AND prevents focus, eliminating the accessibility warning about focused elements being hidden.
2025-10-23 20:48:36 +02:00
Gigi
cf5f8fae16 docs: update CHANGELOG for v0.10.21 2025-10-23 20:43:15 +02:00
Gigi
d9c46e602a chore: bump version to 0.10.21 2025-10-23 20:41:32 +02:00
Gigi
4d980bf91c fix: deduplicate bookmarks in Me component
- Add dedupeBookmarksById to flatten operation in Me.tsx
- Same article can appear in multiple bookmark lists/sets
- Use coordinate-based deduplication (kind:pubkey:identifier) for articles
- Prevents duplicate display when article is in multiple bookmark sources
2025-10-23 20:38:06 +02:00
Gigi
cb3b0e38e9 fix: show article title instead of summary in compact view
- Extract article title from tags in BookmarkItem
- Update CompactView to display title as main text for articles
- Remove unused articleSummary prop from CompactView to keep code DRY
- Follows NIP-23 article metadata structure
2025-10-23 20:29:38 +02:00
Gigi
fbf5c455ca fix: prevent tracking reading position for nostr-event sentinel URLs
- Add check to filter out nostr-event: URLs from reading position tracking
- nostr-event: is an internal sentinel, not a valid Nostr URI per NIP-21
- Prevents these sentinel URLs from being saved to reading position data
2025-10-23 20:21:41 +02:00
Gigi
ed5decf3e9 fix: properly route nostr-event URLs to /e/ path instead of /a/
- Add special handling for nostr-event: URLs in getReadItemUrl
- Add special handling for nostr-event: URLs in handleSelectUrl
- Prevent nostr-event URLs from being incorrectly routed to /a/ path (which expects naddr)
- Route nostr-event URLs to /e/ path for proper event loading
- Fixes 'String must be lowercase or uppercase' error when loading base64-encoded nostr-event URLs
2025-10-23 20:18:35 +02:00
Gigi
44a7e6ae2c docs: update CHANGELOG for v0.10.20 2025-10-23 20:08:49 +02:00
21 changed files with 215 additions and 66 deletions

View File

@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.10.21] - 2025-10-23
### Fixed
- Reading position tracking for internal event URLs
- Prevents tracking for `nostr-event:` sentinel URLs (internal convention, not a valid Nostr URI per NIP-21)
- Fixes "String must be lowercase or uppercase" error when loading base64-encoded event URLs
- These internal sentinels are no longer saved to reading position data
- Compact bookmark view display
- Articles now show title instead of summary in compact view
- Extracts article title from NIP-23 metadata tags
- Removed unused `articleSummary` prop to keep code DRY
- Bookmark deduplication in profile view
- Same article appearing in multiple bookmark lists/sets now displays only once
- Uses coordinate-based deduplication (`kind:pubkey:identifier`) for articles
- Applies `dedupeBookmarksById` when flattening bookmarks from different sources
## [0.10.20] - 2025-10-23
### Added
- Web Bookmarks section now appears first when bookmarks are grouped by source
- Provides quicker access to external URL bookmarks
- Better organization for mixed bookmark collections
### Fixed
- Mobile scroll position preservation when toggling highlights panel
- Opening/closing the highlights sidebar no longer resets scroll to top
- Scroll position is saved before locking and restored after unlocking
- Uses `requestAnimationFrame` to ensure DOM updates before restoring
- Infinite loop in reading position tracking
- Fixed "Maximum update depth exceeded" error in `useReadingPosition` hook
- Callbacks now stored in refs to avoid dependency array issues
- Prevents unnecessary re-renders during scroll tracking
- Skeleton loading state for articles with zero highlights
- Articles without highlights no longer show infinite loading skeletons
- Properly transitions to empty state message
- Navigation to bookmarked articles
- Clicking bookmarked kind:30023 articles now navigates to `/a/:naddr` route
- Fixes issue where clicking showed "Select a bookmark" message instead
- Applies to both compact view and bookmark item interactions
## [0.10.19] - 2025-10-23
### Added

View File

@@ -147,7 +147,7 @@ function generateHtml(naddr: string, meta: ArticleMetadata | null): string {
const baseUrl = 'https://read.withboris.com'
const articleUrl = `${baseUrl}/a/${naddr}`
const title = meta?.title || 'Boris Nostr Bookmarks'
const title = meta?.title || 'Boris Read, Highlight, Explore'
const description = meta?.summary || 'Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights.'
const image = meta?.image?.startsWith('http') ? meta.image : `${baseUrl}${meta?.image || '/boris-social-1200.png'}`
const author = meta?.author || 'Boris'

View File

@@ -9,14 +9,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>Boris - Nostr Bookmarks</title>
<title>Boris - Read, Highlight, Explore</title>
<meta name="description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
<link rel="canonical" href="https://read.withboris.com/" />
<!-- Open Graph / Social Media -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://read.withboris.com/" />
<meta property="og:title" content="Boris - Nostr Bookmarks" />
<meta property="og:title" content="Boris - Read, Highlight, Explore" />
<meta property="og:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
<meta property="og:image" content="https://read.withboris.com/boris-social-1200.png" />
<meta property="og:site_name" content="Boris" />
@@ -24,7 +24,7 @@
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://read.withboris.com/" />
<meta name="twitter:title" content="Boris - Nostr Bookmarks" />
<meta name="twitter:title" content="Boris - Read, Highlight, Explore" />
<meta name="twitter:description" content="Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights." />
<meta name="twitter:image" content="https://read.withboris.com/boris-social-1200.png" />

View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "Boris - Nostr Bookmarks",
"name": "Boris - Read, Highlight, Explore",
"short_name": "Boris",
"description": "Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights.",
"start_url": "/",

View File

@@ -42,10 +42,11 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
const firstUrl = hasUrls ? extractedUrls[0] : null
const firstUrlClassification = firstUrl ? classifyUrl(firstUrl) : null
// For kind:30023 articles, extract image and summary tags (per NIP-23)
// For kind:30023 articles, extract title, image and summary tags (per NIP-23)
// Note: We extract directly from tags here since we don't have the full event.
// When we have full events, we use getArticleImage() helper (see articleService.ts)
const isArticle = bookmark.kind === 30023
const articleTitle = isArticle ? bookmark.tags.find(t => t[0] === 'title')?.[1] : undefined
const articleImage = isArticle ? bookmark.tags.find(t => t[0] === 'image')?.[1] : undefined
const articleSummary = isArticle ? bookmark.tags.find(t => t[0] === 'summary')?.[1] : undefined
@@ -156,10 +157,7 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
hasUrls,
extractedUrls,
onSelectUrl,
authorNpub,
getAuthorDisplayName,
handleReadNow,
articleSummary,
articleTitle,
contentTypeIcon: getContentTypeIcon(),
readingProgress
}

View File

@@ -105,7 +105,18 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
return undefined
}
}
// For web bookmarks and other types, try to use URL if available
// For web bookmarks (kind:39701), URL is in the 'd' tag
if (bookmark.kind === 39701) {
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1]
if (dTag) {
// Ensure URL has protocol
const url = dTag.startsWith('http') ? dTag : `https://${dTag}`
return readingProgressMap.get(url)
}
}
// For other bookmark types, try to extract URL from content
const urls = extractUrlsFromContent(bookmark.content)
if (urls.length > 0) {
return readingProgressMap.get(urls[0])

View File

@@ -13,7 +13,7 @@ interface CompactViewProps {
hasUrls: boolean
extractedUrls: string[]
onSelectUrl?: (url: string, bookmark?: { id: string; kind: number; tags: string[][]; pubkey: string }) => void
articleSummary?: string
articleTitle?: string
contentTypeIcon: IconDefinition
readingProgress?: number
}
@@ -24,7 +24,7 @@ export const CompactView: React.FC<CompactViewProps> = ({
hasUrls,
extractedUrls,
onSelectUrl,
articleSummary,
articleTitle,
contentTypeIcon,
readingProgress
}) => {
@@ -34,7 +34,7 @@ export const CompactView: React.FC<CompactViewProps> = ({
const isNote = bookmark.kind === 1
const isClickable = hasUrls || isArticle || isWebBookmark || isNote
const displayText = isArticle && articleSummary ? articleSummary : bookmark.content
const displayText = isArticle && articleTitle ? articleTitle : bookmark.content
// Calculate progress color
let progressColor = '#6366f1' // Default blue (reading)

View File

@@ -155,6 +155,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const isTextContent = useMemo(() => {
if (loading) return false
if (!markdown && !html) return false
// Don't track internal sentinel URLs (nostr-event: is not a real Nostr URI per NIP-21)
if (selectedUrl?.startsWith('nostr-event:')) return false
if (selectedUrl?.includes('youtube') || selectedUrl?.includes('vimeo')) return false
if (!shouldTrackReadingProgress(html, markdown)) return false

View File

@@ -82,11 +82,28 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
// Visibility filters (defaults from settings or nostrverse when logged out)
const [visibility, setVisibility] = useState<HighlightVisibility>({
nostrverse: activeAccount ? (settings?.defaultExploreScopeNostrverse ?? false) : true,
friends: settings?.defaultExploreScopeFriends ?? true,
mine: settings?.defaultExploreScopeMine ?? false
// Visibility filters - load from localStorage first, fallback to settings
const [visibility, setVisibility] = useState<HighlightVisibility>(() => {
// Try to load from localStorage first
try {
const saved = localStorage.getItem('exploreScopeVisibility')
if (saved) {
const parsed = JSON.parse(saved)
// Validate that at least one scope is enabled
if (parsed.nostrverse || parsed.friends || parsed.mine) {
return parsed
}
}
} catch (err) {
console.warn('Failed to load explore scope from localStorage:', err)
}
// Fallback to settings or defaults
return {
nostrverse: activeAccount ? (settings?.defaultExploreScopeNostrverse ?? false) : true,
friends: settings?.defaultExploreScopeFriends ?? true,
mine: settings?.defaultExploreScopeMine ?? false
}
})
// Ensure at least one scope remains active
@@ -96,6 +113,12 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
if (!next.nostrverse && !next.friends && !next.mine) {
return prev // ignore toggle that would disable all scopes
}
// Persist to localStorage
try {
localStorage.setItem('exploreScopeVisibility', JSON.stringify(next))
} catch (err) {
console.warn('Failed to save explore scope to localStorage:', err)
}
return next
})
}, [])
@@ -224,18 +247,44 @@ const Explore: React.FC<ExploreProps> = ({ relayPool, eventStore, settings, acti
// Update visibility when settings/login state changes
useEffect(() => {
// Check if user has a saved preference
const hasSavedPreference = (() => {
try {
return localStorage.getItem('exploreScopeVisibility') !== null
} catch {
return false
}
})()
// Only reset to defaults if no saved preference exists
if (hasSavedPreference) {
return
}
if (!activeAccount) {
// When logged out, show nostrverse by default
setVisibility(prev => ({ ...prev, nostrverse: true, friends: false, mine: false }))
const defaultVisibility = { nostrverse: true, friends: false, mine: false }
setVisibility(defaultVisibility)
try {
localStorage.setItem('exploreScopeVisibility', JSON.stringify(defaultVisibility))
} catch (err) {
console.warn('Failed to save explore scope to localStorage:', err)
}
setHasLoadedNostrverse(true) // logged out path loads nostrverse immediately
setHasLoadedNostrverseHighlights(true)
} else {
// When logged in, use settings defaults immediately
setVisibility({
const defaultVisibility = {
nostrverse: settings?.defaultExploreScopeNostrverse ?? false,
friends: settings?.defaultExploreScopeFriends ?? true,
mine: settings?.defaultExploreScopeMine ?? false
})
}
setVisibility(defaultVisibility)
try {
localStorage.setItem('exploreScopeVisibility', JSON.stringify(defaultVisibility))
} catch (err) {
console.warn('Failed to save explore scope to localStorage:', err)
}
setHasLoadedNostrverse(false)
setHasLoadedNostrverseHighlights(false)
}

View File

@@ -45,7 +45,7 @@ export const HighlightButton = React.forwardRef<HighlightButtonRef, HighlightBut
className="highlight-fab"
style={{
position: 'fixed',
bottom: '32px',
bottom: '80px',
right: '32px',
zIndex: 1000,
width: '56px',

View File

@@ -25,6 +25,7 @@ import { faBooks } from '../icons/customIcons'
import { usePullToRefresh } from 'use-pull-to-refresh'
import RefreshIndicator from './RefreshIndicator'
import { groupIndividualBookmarks, hasContent, hasCreationDate, sortIndividualBookmarks } from '../utils/bookmarkUtils'
import { dedupeBookmarksById } from '../services/bookmarkHelpers'
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
import ReadingProgressFilters, { ReadingProgressFilterType } from './ReadingProgressFilters'
@@ -394,8 +395,7 @@ const Me: React.FC<MeProps> = ({
}
const getReadItemUrl = (item: ReadItem) => {
if (item.type === 'article') {
// ID is already in naddr format
if (item.type === 'article' && item.id.startsWith('naddr1')) {
return `/a/${item.id}`
} else if (item.url) {
return `/r/${encodeURIComponent(item.url)}`
@@ -438,19 +438,16 @@ const Me: React.FC<MeProps> = ({
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
const dTag = bookmark.tags.find(t => t[0] === 'd')?.[1] || ''
if (dTag && bookmark.pubkey) {
const pointer = {
identifier: dTag,
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: bookmark.pubkey,
}
const naddr = nip19.naddrEncode(pointer)
identifier: dTag
})
navigate(`/a/${naddr}`)
}
} else if (url) {
// For regular URLs, navigate to the reader route
navigate(`/r/${encodeURIComponent(url)}`)
}
}
@@ -491,8 +488,10 @@ const Me: React.FC<MeProps> = ({
return undefined
}
// Merge and flatten all individual bookmarks
const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
// Merge and flatten all individual bookmarks with deduplication
const allIndividualBookmarks = dedupeBookmarksById(
bookmarks.flatMap(b => b.individualBookmarks || [])
)
.filter(hasContent)
.filter(b => !settings?.hideBookmarksWithoutCreationDate || hasCreationDate(b))

View File

@@ -55,6 +55,10 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
const handleMenuItemClick = (action: () => void) => {
setShowProfileMenu(false)
// Close mobile sidebar when navigating on mobile
if (isMobile) {
onToggleCollapse()
}
action()
}
@@ -127,23 +131,38 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
<div className="sidebar-header-right">
<IconButton
icon={faHome}
onClick={() => navigate('/')}
onClick={() => {
if (isMobile) {
onToggleCollapse()
}
navigate('/')
}}
title="Home"
ariaLabel="Home"
variant="ghost"
/>
<IconButton
icon={faGear}
onClick={onOpenSettings}
title="Settings"
ariaLabel="Settings"
icon={faPersonHiking}
onClick={() => {
if (isMobile) {
onToggleCollapse()
}
navigate('/explore')
}}
title="Explore"
ariaLabel="Explore"
variant="ghost"
/>
<IconButton
icon={faPersonHiking}
onClick={() => navigate('/explore')}
title="Explore"
ariaLabel="Explore"
icon={faGear}
onClick={() => {
if (isMobile) {
onToggleCollapse()
}
onOpenSettings()
}}
title="Settings"
ariaLabel="Settings"
variant="ghost"
/>
{!isMobile && (

View File

@@ -321,7 +321,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
<div
ref={sidebarRef}
className={`pane sidebar ${isMobile && props.isSidebarOpen ? 'mobile-open' : ''}`}
aria-hidden={isMobile && !props.isSidebarOpen}
{...(isMobile && !props.isSidebarOpen ? { inert: '' } : {})}
>
<BookmarkList
bookmarks={props.bookmarks}
@@ -413,7 +413,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
<div
ref={highlightsRef}
className={`pane highlights ${isMobile && !props.isHighlightsCollapsed ? 'mobile-open' : ''}`}
aria-hidden={isMobile && props.isHighlightsCollapsed}
{...(isMobile && props.isHighlightsCollapsed ? { inert: '' } : {})}
>
<HighlightsPanel
highlights={props.highlights}

View File

@@ -28,7 +28,7 @@ export const fetchHighlightsFromAuthors = async (
const seenIds = new Set<string>()
const rawEvents = await queryEvents(
relayPool,
{ kinds: [9802], authors: pubkeys, limit: 200 },
{ kinds: [9802], authors: pubkeys, limit: 1000 },
{
onEvent: (event: NostrEvent) => {
if (!seenIds.has(event.id)) {

View File

@@ -81,13 +81,21 @@ class NostrverseHighlightsController {
const currentGeneration = this.generation
this.setLoading(true)
try {
const seenIds = new Set<string>()
const highlightsMap = new Map<string, Highlight>()
try {
const seenIds = new Set<string>()
// Start with existing highlights when doing incremental sync
const highlightsMap = new Map<string, Highlight>(
this.currentHighlights.map(h => [h.id, h])
)
const lastSyncedAt = force ? null : this.getLastSyncedAt()
const filter: { kinds: number[]; since?: number } = { kinds: [KINDS.Highlights] }
if (lastSyncedAt) filter.since = lastSyncedAt
const lastSyncedAt = force ? null : this.getLastSyncedAt()
const filter: { kinds: number[]; since?: number; limit?: number } = { kinds: [KINDS.Highlights] }
if (lastSyncedAt) {
filter.since = lastSyncedAt
} else {
// On initial load, fetch more highlights
filter.limit = 1000
}
const events = await queryEvents(
relayPool,
@@ -111,22 +119,24 @@ class NostrverseHighlightsController {
if (currentGeneration !== this.generation) return
events.forEach(evt => eventStore.add(evt))
events.forEach(evt => eventStore.add(evt))
const highlights = events.map(eventToHighlight)
const unique = Array.from(new Map(highlights.map(h => [h.id, h])).values())
const sorted = sortHighlights(unique)
const highlights = events.map(eventToHighlight)
// Merge new highlights with existing ones
highlights.forEach(h => highlightsMap.set(h.id, h))
const sorted = sortHighlights(Array.from(highlightsMap.values()))
this.currentHighlights = sorted
this.loaded = true
this.emitHighlights(sorted)
this.currentHighlights = sorted
this.loaded = true
this.emitHighlights(sorted)
if (sorted.length > 0) {
const newest = Math.max(...sorted.map(h => h.created_at))
this.setLastSyncedAt(newest)
}
} catch (err) {
this.currentHighlights = []
// On error, keep existing highlights instead of clearing them
console.error('[nostrverse-highlights] Failed to sync:', err)
this.emitHighlights(this.currentHighlights)
} finally {
if (currentGeneration === this.generation) this.setLoading(false)

View File

@@ -19,7 +19,6 @@ export interface ReadingProgressContent {
progress: number // 0-1 scroll progress
ts?: number // Unix timestamp (optional, for display)
loc?: number // Optional: pixel position
ver?: string // Schema version
}
// Helper to extract and parse reading progress from event (kind 39802)
@@ -117,8 +116,7 @@ export async function saveReadingPosition(
const progressContent: ReadingProgressContent = {
progress: position.position,
ts: position.timestamp,
loc: position.scrollTop,
ver: '1'
loc: position.scrollTop
}
const tags = generateProgressTags(articleIdentifier)

View File

@@ -13,7 +13,7 @@ type ProgressMapCallback = (progressMap: Map<string, number>) => void
type LoadingCallback = (loading: boolean) => void
const LAST_SYNCED_KEY = 'reading_progress_last_synced'
const PROGRESS_CACHE_KEY = 'reading_progress_cache_v1'
const PROGRESS_CACHE_KEY = 'reading_progress_cache'
/**
* Shared reading progress controller

View File

@@ -81,7 +81,15 @@ export function applyRelaySetToPool(
for (const url of toRemove) {
const relay = relayPool.relays.get(url)
if (relay) {
relay.close()
try {
// Only close if relay is actually connected or attempting to connect
// This helps avoid WebSocket warnings for connections that never started
relay.close()
} catch (error) {
// Suppress errors when closing relays that haven't fully connected yet
// This can happen when switching relay sets before connections establish
console.debug('[relay-manager] Ignoring error when closing relay:', url, error)
}
relayPool.relays.delete(url)
}
}

View File

@@ -75,6 +75,18 @@
.me-highlights-list { padding-left: 0; padding-right: 0; }
.explore-header .author-card { max-width: 600px; margin: 0 auto; width: 100%; }
/* Hide tab labels on mobile to save space */
@media (max-width: 768px) {
.me-tab .tab-label {
display: none;
}
.me-tab {
padding: 0.75rem;
gap: 0;
}
}
/* Bookmarks list */
.bookmarks-list {
display: flex;

View File

@@ -104,7 +104,7 @@ export default defineConfig({
filename: 'sw.ts',
injectRegister: null,
manifest: {
name: 'Boris - Nostr Bookmarks',
name: 'Boris - Read, Highlight, Explore',
short_name: 'Boris',
description: 'Your reading list for the Nostr world. A minimal nostr client for bookmark management with highlights.',
start_url: '/',