mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 20:45:01 +01:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1800ee324e | ||
|
|
7d2dac2f1a | ||
|
|
7875f1d0bd | ||
|
|
d9263e07d1 | ||
|
|
9a345a7347 | ||
|
|
55d1af3bf9 | ||
|
|
feb3134b65 | ||
|
|
7d222e099f | ||
|
|
59436b5b9e | ||
|
|
2e08954e83 | ||
|
|
9cb1791a3a | ||
|
|
28ba620967 | ||
|
|
56f2d33e93 | ||
|
|
312c742969 | ||
|
|
0781c4ebfc | ||
|
|
85f4cd3590 | ||
|
|
89bc6258b1 | ||
|
|
534b628aea | ||
|
|
317d2e0b53 | ||
|
|
9ea69589fa | ||
|
|
89eaa97d30 | ||
|
|
0283405fb5 | ||
|
|
5eade913d1 |
121
CHANGELOG.md
121
CHANGELOG.md
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.10.18] - 2025-10-23
|
||||
|
||||
### Changed
|
||||
|
||||
- User profile routes renamed from `/me` to `/my`
|
||||
- `/my/highlights` - User's highlights
|
||||
- `/my/bookmarks` - User's bookmarks
|
||||
- `/my/reads` - Nostr-native articles with reading progress
|
||||
- `/my/links` - External URLs with reading progress
|
||||
- `/my/writings` - User's published articles
|
||||
- All navigation, tabs, and internal links updated
|
||||
- Documentation updated to reflect new paths
|
||||
|
||||
### Fixed
|
||||
|
||||
- `/my/writings` now displays all user writings
|
||||
- Removed incremental loading with `since` filters from writingsController
|
||||
- Controller now fetches ALL writings without limits
|
||||
- Ensures complete results on profile and my pages
|
||||
- `/my/highlights` now displays all user highlights
|
||||
- Removed incremental loading with `since` filters from highlightsController
|
||||
- Controller now fetches ALL highlights without limits
|
||||
- Consistent behavior across all profile views
|
||||
|
||||
### Refactored
|
||||
|
||||
- Centralized data fetching in controllers
|
||||
- Removed duplicate background fetch logic from Me.tsx and Profile.tsx
|
||||
- All writings fetching now handled by writingsController
|
||||
- All highlights fetching now handled by highlightsController
|
||||
- Single source of truth following controller pattern: stream, dedupe, store, emit
|
||||
- Cleaner, more maintainable code (DRY principle)
|
||||
|
||||
## [0.10.17] - 2025-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- Setting to control auto-scroll to reading position
|
||||
- New toggle in Settings > Reading Experience
|
||||
- Allows users to disable automatic scroll restoration
|
||||
- Defaults to enabled (preserves existing behavior)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Blockquote styling on mobile devices
|
||||
- Added equal right padding to match left padding (2rem on both sides)
|
||||
- Prevents awkward text cutoff on narrow screens
|
||||
- Timestamp clicks in highlight cards now navigate within app
|
||||
- Articles (kind:30023) open via `/a/{naddr}` route
|
||||
- External URLs open via `/r/{encodedUrl}` route
|
||||
- Previously opened external search portal (ants.sh)
|
||||
- Highlight automatically scrolls into view with sidebar open
|
||||
- Hero images now properly extend edge-to-edge on mobile
|
||||
- Adjusted negative margins to match new reader padding
|
||||
- Image bleeds to screen edges while text maintains comfortable margins
|
||||
- Article relay links now open via `/a/` path instead of `/r/`
|
||||
- Ensures nostr-native articles route correctly
|
||||
- External links continue to use `/r/` path
|
||||
|
||||
### Changed
|
||||
|
||||
- Mobile reader padding increased for better readability
|
||||
- Horizontal padding increased from 0.5rem to 1rem
|
||||
- Title, summary, and body text now properly aligned
|
||||
- More comfortable reading experience on small screens
|
||||
- Reading position save interval reduced from 3s to 1s
|
||||
- More frequent auto-saves during active reading
|
||||
- Better preservation of reading progress
|
||||
|
||||
## [0.10.16] - 2025-10-22
|
||||
|
||||
### Fixed
|
||||
@@ -34,7 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Highlights now scroll into view when clicked from `/me/highlights` page
|
||||
- Highlights now scroll into view when clicked from `/my/highlights` page
|
||||
- Navigation state properly passes highlight ID and openHighlights flag
|
||||
- Works for both article links and external URL links
|
||||
|
||||
@@ -159,8 +228,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
|
||||
- Default bookmark view changed to flat chronological list (newest first)
|
||||
- Bookmark URL changed from `/me/reading-list` to `/me/bookmarks`
|
||||
- Router updated to handle `/me/reading-list` → `/me/bookmarks` redirect
|
||||
- Bookmark URL changed from `/my/reading-list` to `/my/bookmarks`
|
||||
- Router updated to handle `/my/reading-list` → `/my/bookmarks` redirect
|
||||
- Me.tsx bookmarks tab now uses dynamic filter titles and chronological sorting
|
||||
- Me.tsx updated to use faClock icon instead of faBars
|
||||
- Removed bookmark count from section headings for cleaner display
|
||||
@@ -266,7 +335,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Tab switching regression on `/me` page
|
||||
- Tab switching regression on `/my` page
|
||||
- Resolved infinite update loop caused by circular dependency in `useCallback` hooks
|
||||
- Tab navigation now properly updates UI when URL changes
|
||||
- Removed `loadedTabs` from dependency arrays to prevent re-render cycles
|
||||
@@ -624,7 +693,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- `/me/bookmarks` tab now displays in cards view only
|
||||
- `/my/bookmarks` tab now displays in cards view only
|
||||
- Removed view mode toggle buttons (compact, large) from bookmarks tab
|
||||
- Cards view provides optimal bookmark browsing experience
|
||||
- Grouping toggle (grouped/flat) still available
|
||||
@@ -737,7 +806,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent "No highlights yet" flash on `/me/highlights` page
|
||||
- Prevent "No highlights yet" flash on `/my/highlights` page
|
||||
- Force React to remount tab content when switching tabs for proper state management
|
||||
- Deduplicate blog posts by author:d-tag instead of event ID for better accuracy
|
||||
- Show skeleton placeholders while highlights are loading for better UX
|
||||
@@ -945,7 +1014,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Filter icons colored when active (blue for most, green for completed)
|
||||
- URL routing support for reading progress filters
|
||||
- Reading progress filters available in Archive tab and bookmarks sidebar
|
||||
- Reads and Links tabs on `/me` page
|
||||
- Reads and Links tabs on `/my` page
|
||||
- Reads tab shows nostr-native articles with reading progress
|
||||
- Links tab shows external URLs with reading progress
|
||||
- Both tabs populate instantly from bookmarks for fast loading
|
||||
@@ -991,7 +1060,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Bookmark filter buttons by content type (articles, videos, images, web links)
|
||||
- Filter bookmarks by their content type on bookmarks sidebar
|
||||
- Filters also available on `/me` page bookmarks tab
|
||||
- Filters also available on `/my` page bookmarks tab
|
||||
- Separate filter for external articles with link icon
|
||||
- Multiple filters can be active simultaneously
|
||||
- Private Bookmarks section for encrypted legacy bookmarks
|
||||
@@ -1005,7 +1074,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Better categorization of bookmark types
|
||||
- Bookmark filter button styling refined
|
||||
- Reduced whitespace around bookmark filters for cleaner layout
|
||||
- Dramatically reduced whitespace on both sidebar and `/me` page
|
||||
- Dramatically reduced whitespace on both sidebar and `/my` page
|
||||
- Lock icon removed from individual bookmarks
|
||||
- Encryption status now indicated by section grouping
|
||||
- Cleaner bookmark item appearance
|
||||
@@ -1172,7 +1241,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Camera icon for image bookmarks
|
||||
- Sticky note icon for text-only bookmarks without URLs
|
||||
- Bookmark grouping and sections
|
||||
- Grouped sections in sidebar and `/me` reading-list
|
||||
- Grouped sections in sidebar and `/my` reading-list
|
||||
- Web bookmarks, default bookmarks, and legacy bookmarks in separate sections
|
||||
- Grouping and sorting helpers for organizing bookmark sections
|
||||
- Adaptive text color for publication date over hero images
|
||||
@@ -1235,7 +1304,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Mobile bookmark button visibility across all pages
|
||||
- Now visible on `/p/` (profile), `/explore`, `/me`, and `/support` pages
|
||||
- Now visible on `/p/` (profile), `/explore`, `/my`, and `/support` pages
|
||||
- Only hidden on settings page or when scrolling down while reading
|
||||
- Prevents users from getting stuck without navigation options
|
||||
- Mobile highlights button behavior at page top
|
||||
@@ -1416,7 +1485,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Highlights tab on `/explore` page
|
||||
- View highlights from friends and followed users
|
||||
- Tab structure matching `/me` and profile pages
|
||||
- Tab structure matching `/my` and profile pages
|
||||
- Grid layout for highlights with cards
|
||||
- Highlights shown first, writings second
|
||||
- Clicking highlight opens source article and scrolls to position
|
||||
@@ -1573,7 +1642,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Writings tab on `/me` page to display user's published articles
|
||||
- Writings tab on `/my` page to display user's published articles
|
||||
- Comprehensive headline styling (h1-h6) with Tailwind typography
|
||||
- List styling for ordered and unordered lists in articles
|
||||
- Blockquote styling with indentation and italics
|
||||
@@ -1596,7 +1665,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Horizontal overflow from code blocks and wide content on mobile
|
||||
- Settings view now mobile-friendly with proper width constraints
|
||||
- Long relay URLs no longer cause horizontal overflow on mobile
|
||||
- Sidebar/highlights toggle buttons hidden on settings/explore/me pages
|
||||
- Sidebar/highlights toggle buttons hidden on settings/explore/my pages
|
||||
- Video titles now show filename instead of 'Error Loading Content'
|
||||
- AddBookmarkModal z-index issue fixed using React Portal
|
||||
- Highlight matching for text spanning multiple DOM nodes/inline elements
|
||||
@@ -1651,7 +1720,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- YouTube video metadata extraction with title, description, and captions
|
||||
- Responsive video player with aspect ratio support
|
||||
- Thumbnail images in compact view
|
||||
- URL routing for /me page tabs
|
||||
- URL routing for /my page tabs
|
||||
- Bookmark navigation in reading list
|
||||
- Video duration display for video URLs
|
||||
- Three-dot menu for videos with open/native/copy/share actions
|
||||
@@ -1681,7 +1750,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Style
|
||||
|
||||
- Hide tab counts on mobile for /me page
|
||||
- Hide tab counts on mobile for /my page
|
||||
- Remove max-width on main pane, constrain reader instead
|
||||
- Full width layout for videos
|
||||
- Reader-video specific styles
|
||||
@@ -1694,11 +1763,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Prism.js syntax highlighting for code blocks
|
||||
- Inline image rendering in nostr-native blog posts
|
||||
- Image placeholders on blog post cards in `/explore`
|
||||
- Caching on `/me` page for faster loading
|
||||
- Caching on `/my` page for faster loading
|
||||
|
||||
### Changed
|
||||
|
||||
- Reading List on `/me` now uses the same components as the bookmarks sidebar
|
||||
- Reading List on `/my` now uses the same components as the bookmarks sidebar
|
||||
- Improve bookmarks sidebar visual design
|
||||
- Make article menu button more subtle by removing border
|
||||
|
||||
@@ -1718,8 +1787,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- `/me` page with tabbed layout featuring Highlights, Reading List, and Library tabs
|
||||
- Two-pane layout for `/me` page with article sources and highlights
|
||||
- `/my` page with tabbed layout featuring Highlights, Reading List, and Library tabs
|
||||
- Two-pane layout for `/my` page with article sources and highlights
|
||||
- Custom FontAwesome Pro books icon for Archive tab
|
||||
- CompactButton component for highlight cards
|
||||
- Instant mark-as-read functionality with checkmark animation and read status checking
|
||||
@@ -1728,7 +1797,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Rename Library tab to Archive
|
||||
- Move highlight timestamp to top-right corner of cards
|
||||
- Replace username with AuthorCard component on `/me` page
|
||||
- Replace username with AuthorCard component on `/my` page
|
||||
- Use user's custom highlight color for Highlights tab
|
||||
- Render library articles using BlogPostCard component for consistency
|
||||
- Use faBooks icon for Mark as Read button
|
||||
@@ -1748,12 +1817,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Style
|
||||
|
||||
- Match `/me` profile card width to highlight cards
|
||||
- Improve Me page mobile tabs and avoid overlap with sidebar buttons
|
||||
- Match `/my` profile card width to highlight cards
|
||||
- Improve My page mobile tabs and avoid overlap with sidebar buttons
|
||||
- Reduce margins/paddings to make highlight cards more compact
|
||||
- Tighten vertical spacing on highlight cards
|
||||
- Left-align text inside author card
|
||||
- Constrain `/me` page content width to match author card (600px)
|
||||
- Constrain `/my` page content width to match author card (600px)
|
||||
- Improve tab border styling for dark theme
|
||||
- Make relay indicator match CompactButton (same look as menu)
|
||||
- Align relay indicator within footer with symmetric spacing
|
||||
@@ -1858,7 +1927,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Highlights: merge remote results after local for article/url
|
||||
- Explore: always query remote relays after local; stream merge into UI
|
||||
- Improve mobile touch targets for highlight icons
|
||||
- Color `/me` highlights with "my highlights" color setting
|
||||
- Color `/my` highlights with "my highlights" color setting
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -1890,7 +1959,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Confirmation dialog prevents accidental deletions
|
||||
- Styled to match relay indicator (subtle, same size)
|
||||
- Removes highlights from UI immediately after deletion request
|
||||
- `/me` page showing user's recent highlights
|
||||
- `/my` page showing user's recent highlights
|
||||
- Accessible by clicking profile picture in bookmark sidebar
|
||||
- Displays all highlights created by the logged-in user
|
||||
- Uses same rendering as Settings and Explore pages
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
- **Explore**: Discover friends' highlights and writings, plus a "nostrverse" feed.
|
||||
- **Filters**: Visibility toggles (mine, friends, nostrverse) apply to Explore highlights.
|
||||
- **Profiles**: View your own (`/me`) or other users (`/p/:npub`) with tabs for Highlights, Bookmarks, Archive, and Writings.
|
||||
- **Profiles**: View your own (`/my`) or other users (`/p/:npub`) with tabs for Highlights, Bookmarks, Archive, and Writings.
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.10.17",
|
||||
"version": "0.10.19",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"homepage": "https://read.withboris.com/",
|
||||
"type": "module",
|
||||
|
||||
18
src/App.tsx
18
src/App.tsx
@@ -237,11 +237,11 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me"
|
||||
element={<Navigate to="/me/highlights" replace />}
|
||||
path="/my"
|
||||
element={<Navigate to="/my/highlights" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/me/highlights"
|
||||
path="/my/highlights"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
@@ -253,7 +253,7 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me/bookmarks"
|
||||
path="/my/bookmarks"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
@@ -265,7 +265,7 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me/reads"
|
||||
path="/my/reads"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
@@ -277,7 +277,7 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me/reads/:filter"
|
||||
path="/my/reads/:filter"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
@@ -289,7 +289,7 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me/links"
|
||||
path="/my/links"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
@@ -301,7 +301,7 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me/links/:filter"
|
||||
path="/my/links/:filter"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
@@ -313,7 +313,7 @@ function AppRoutes({
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/me/writings"
|
||||
path="/my/writings"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useRef, useState, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronLeft, faBookmark, faList, faThLarge, faImage, faRotate, faHeart, faPlus, faLayerGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronLeft, faBookmark, faList, faThLarge, faImage, faHeart, faPlus, faLayerGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faClock } from '@fortawesome/free-regular-svg-icons'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { Bookmark, IndividualBookmark } from '../types/bookmarks'
|
||||
import { BookmarkItem } from './BookmarkItem'
|
||||
@@ -59,7 +58,6 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
onOpenSettings,
|
||||
onRefresh,
|
||||
isRefreshing,
|
||||
lastFetchTime,
|
||||
loading = false,
|
||||
relayPool,
|
||||
isMobile = false,
|
||||
@@ -314,9 +312,7 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
variant="ghost"
|
||||
style={{ color: friendsColor }}
|
||||
/>
|
||||
</div>
|
||||
{activeAccount && (
|
||||
<div className="view-mode-right">
|
||||
{activeAccount && (
|
||||
<IconButton
|
||||
icon={groupingMode === 'grouped' ? faLayerGroup : faClock}
|
||||
onClick={toggleGroupingMode}
|
||||
@@ -324,17 +320,10 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
ariaLabel={groupingMode === 'grouped' ? 'Switch to flat view' : 'Switch to grouped view'}
|
||||
variant="ghost"
|
||||
/>
|
||||
{onRefresh && (
|
||||
<IconButton
|
||||
icon={faRotate}
|
||||
onClick={onRefresh}
|
||||
title={lastFetchTime ? `Refresh bookmarks (updated ${formatDistanceToNow(lastFetchTime, { addSuffix: true })})` : 'Refresh bookmarks'}
|
||||
ariaLabel="Refresh bookmarks"
|
||||
variant="ghost"
|
||||
disabled={isRefreshing}
|
||||
spin={isRefreshing}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
{activeAccount && (
|
||||
<div className="view-mode-right">
|
||||
<IconButton
|
||||
icon={faList}
|
||||
onClick={() => onViewModeChange('compact')}
|
||||
|
||||
@@ -53,7 +53,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({
|
||||
|
||||
const showSettings = location.pathname === '/settings'
|
||||
const showExplore = location.pathname.startsWith('/explore')
|
||||
const showMe = location.pathname.startsWith('/me')
|
||||
const showMe = location.pathname.startsWith('/my')
|
||||
const showProfile = location.pathname.startsWith('/p/')
|
||||
const showSupport = location.pathname === '/support'
|
||||
const eventId = eventIdParam
|
||||
@@ -62,12 +62,12 @@ const Bookmarks: React.FC<BookmarksProps> = ({
|
||||
const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights'
|
||||
|
||||
// Extract tab from me routes
|
||||
const meTab = location.pathname === '/me' ? 'highlights' :
|
||||
location.pathname === '/me/highlights' ? 'highlights' :
|
||||
location.pathname === '/me/bookmarks' ? 'bookmarks' :
|
||||
location.pathname.startsWith('/me/reads') ? 'reads' :
|
||||
location.pathname.startsWith('/me/links') ? 'links' :
|
||||
location.pathname === '/me/writings' ? 'writings' : 'highlights'
|
||||
const meTab = location.pathname === '/my' ? 'highlights' :
|
||||
location.pathname === '/my/highlights' ? 'highlights' :
|
||||
location.pathname === '/my/bookmarks' ? 'bookmarks' :
|
||||
location.pathname.startsWith('/my/reads') ? 'reads' :
|
||||
location.pathname.startsWith('/my/links') ? 'links' :
|
||||
location.pathname === '/my/writings' ? 'writings' : 'highlights'
|
||||
|
||||
// Extract tab from profile routes
|
||||
const profileTab = location.pathname.endsWith('/writings') ? 'writings' : 'highlights'
|
||||
@@ -87,7 +87,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Track previous location for going back from settings/me/explore/profile
|
||||
// Track previous location for going back from settings/my/explore/profile
|
||||
useEffect(() => {
|
||||
if (!showSettings && !showMe && !showExplore && !showProfile) {
|
||||
previousLocationRef.current = location.pathname
|
||||
|
||||
@@ -118,13 +118,11 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
return (
|
||||
<div className="highlights-container">
|
||||
<HighlightsPanelHeader
|
||||
loading={loading}
|
||||
hasHighlights={filteredHighlights.length > 0}
|
||||
showHighlights={showHighlights}
|
||||
highlightVisibility={highlightVisibility}
|
||||
currentUserPubkey={currentUserPubkey}
|
||||
onToggleHighlights={handleToggleHighlights}
|
||||
onRefresh={onRefresh}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
onHighlightVisibilityChange={onHighlightVisibilityChange}
|
||||
isMobile={isMobile}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import React from 'react'
|
||||
import { faChevronRight, faEye, faEyeSlash, faRotate, faUser, faUserGroup, faNetworkWired } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faEye, faEyeSlash, faUser, faUserGroup, faNetworkWired } from '@fortawesome/free-solid-svg-icons'
|
||||
import { HighlightVisibility } from '../HighlightsPanel'
|
||||
import IconButton from '../IconButton'
|
||||
|
||||
interface HighlightsPanelHeaderProps {
|
||||
loading: boolean
|
||||
hasHighlights: boolean
|
||||
showHighlights: boolean
|
||||
highlightVisibility: HighlightVisibility
|
||||
currentUserPubkey?: string
|
||||
onToggleHighlights: () => void
|
||||
onRefresh?: () => void
|
||||
onToggleCollapse: () => void
|
||||
onHighlightVisibilityChange?: (visibility: HighlightVisibility) => void
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
loading,
|
||||
hasHighlights,
|
||||
showHighlights,
|
||||
highlightVisibility,
|
||||
currentUserPubkey,
|
||||
onToggleHighlights,
|
||||
onRefresh,
|
||||
onToggleCollapse,
|
||||
onHighlightVisibilityChange,
|
||||
isMobile = false
|
||||
@@ -32,6 +29,16 @@ const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
<div className="highlights-header">
|
||||
<div className="highlights-actions">
|
||||
<div className="highlights-actions-left">
|
||||
{!isMobile && (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="toggle-highlights-btn"
|
||||
title="Collapse highlights panel"
|
||||
aria-label="Collapse highlights panel"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronRight} style={{ transform: 'rotate(180deg)' }} />
|
||||
</button>
|
||||
)}
|
||||
{onHighlightVisibilityChange && (
|
||||
<div className="highlight-level-toggles">
|
||||
<IconButton
|
||||
@@ -82,17 +89,8 @@ const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{onRefresh && (
|
||||
<IconButton
|
||||
icon={faRotate}
|
||||
onClick={onRefresh}
|
||||
title="Refresh highlights"
|
||||
ariaLabel="Refresh highlights"
|
||||
variant="ghost"
|
||||
disabled={loading}
|
||||
spin={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="highlights-actions-right">
|
||||
{hasHighlights && (
|
||||
<IconButton
|
||||
icon={showHighlights ? faEye : faEyeSlash}
|
||||
@@ -103,16 +101,6 @@ const HighlightsPanelHeader: React.FC<HighlightsPanelHeaderProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<IconButton
|
||||
icon={faChevronRight}
|
||||
onClick={onToggleCollapse}
|
||||
title="Collapse highlights panel"
|
||||
ariaLabel="Collapse highlights panel"
|
||||
variant="ghost"
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faHighlighter, faBookmark, faPenToSquare, faLink, faLayerGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faHighlighter, faBookmark, faPenToSquare, faLink, faLayerGroup, faHeart } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faClock } from '@fortawesome/free-regular-svg-icons'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { IEventStore } from 'applesauce-core'
|
||||
@@ -144,15 +144,15 @@ const Me: React.FC<MeProps> = ({
|
||||
setReadingProgressFilter(filter)
|
||||
if (activeTab === 'reads') {
|
||||
if (filter === 'all') {
|
||||
navigate('/me/reads', { replace: true })
|
||||
navigate('/my/reads', { replace: true })
|
||||
} else {
|
||||
navigate(`/me/reads/${filter}`, { replace: true })
|
||||
navigate(`/my/reads/${filter}`, { replace: true })
|
||||
}
|
||||
} else if (activeTab === 'links') {
|
||||
if (filter === 'all') {
|
||||
navigate('/me/links', { replace: true })
|
||||
navigate('/my/links', { replace: true })
|
||||
} else {
|
||||
navigate(`/me/links/${filter}`, { replace: true })
|
||||
navigate(`/my/links/${filter}`, { replace: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -668,21 +668,26 @@ const Me: React.FC<MeProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)))}
|
||||
<div className="view-mode-controls" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '1rem',
|
||||
marginTop: '1rem',
|
||||
borderTop: '1px solid var(--border-color)'
|
||||
}}>
|
||||
<IconButton
|
||||
icon={groupingMode === 'grouped' ? faLayerGroup : faClock}
|
||||
onClick={toggleGroupingMode}
|
||||
title={groupingMode === 'grouped' ? 'Show flat chronological list' : 'Show grouped by source'}
|
||||
ariaLabel={groupingMode === 'grouped' ? 'Switch to flat view' : 'Switch to grouped view'}
|
||||
variant="ghost"
|
||||
/>
|
||||
<div className="view-mode-controls">
|
||||
<div className="view-mode-left">
|
||||
<IconButton
|
||||
icon={faHeart}
|
||||
onClick={() => navigate('/support')}
|
||||
title="Support Boris"
|
||||
ariaLabel="Support"
|
||||
variant="ghost"
|
||||
style={{ color: 'rgb(251 146 60)' }}
|
||||
/>
|
||||
<IconButton
|
||||
icon={groupingMode === 'grouped' ? faLayerGroup : faClock}
|
||||
onClick={toggleGroupingMode}
|
||||
title={groupingMode === 'grouped' ? 'Show flat chronological list' : 'Show grouped by source'}
|
||||
ariaLabel={groupingMode === 'grouped' ? 'Switch to flat view' : 'Switch to grouped view'}
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<div className="view-mode-right">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -867,7 +872,7 @@ const Me: React.FC<MeProps> = ({
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'highlights' ? 'active' : ''}`}
|
||||
data-tab="highlights"
|
||||
onClick={() => navigate('/me/highlights')}
|
||||
onClick={() => navigate('/my/highlights')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHighlighter} />
|
||||
<span className="tab-label">Highlights</span>
|
||||
@@ -875,7 +880,7 @@ const Me: React.FC<MeProps> = ({
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'bookmarks' ? 'active' : ''}`}
|
||||
data-tab="bookmarks"
|
||||
onClick={() => navigate('/me/bookmarks')}
|
||||
onClick={() => navigate('/my/bookmarks')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBookmark} />
|
||||
<span className="tab-label">Bookmarks</span>
|
||||
@@ -883,7 +888,7 @@ const Me: React.FC<MeProps> = ({
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'reads' ? 'active' : ''}`}
|
||||
data-tab="reads"
|
||||
onClick={() => navigate('/me/reads')}
|
||||
onClick={() => navigate('/my/reads')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBooks} />
|
||||
<span className="tab-label">Reads</span>
|
||||
@@ -891,7 +896,7 @@ const Me: React.FC<MeProps> = ({
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'links' ? 'active' : ''}`}
|
||||
data-tab="links"
|
||||
onClick={() => navigate('/me/links')}
|
||||
onClick={() => navigate('/my/links')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} />
|
||||
<span className="tab-label">Links</span>
|
||||
@@ -899,7 +904,7 @@ const Me: React.FC<MeProps> = ({
|
||||
<button
|
||||
className={`me-tab ${activeTab === 'writings' ? 'active' : ''}`}
|
||||
data-tab="writings"
|
||||
onClick={() => navigate('/me/writings')}
|
||||
onClick={() => navigate('/my/writings')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPenToSquare} />
|
||||
<span className="tab-label">Writings</span>
|
||||
|
||||
@@ -6,10 +6,8 @@ import { RelayPool } from 'applesauce-relay'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { HighlightItem } from './HighlightItem'
|
||||
import { BlogPostPreview, fetchBlogPostsFromAuthors } from '../services/exploreService'
|
||||
import { fetchHighlights } from '../services/highlightService'
|
||||
import { BlogPostPreview } from '../services/exploreService'
|
||||
import { KINDS } from '../config/kinds'
|
||||
import { getActiveRelayUrls } from '../services/relayManager'
|
||||
import AuthorCard from './AuthorCard'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import { BlogPostSkeleton, HighlightSkeleton } from './Skeletons'
|
||||
@@ -20,6 +18,8 @@ import { usePullToRefresh } from 'use-pull-to-refresh'
|
||||
import RefreshIndicator from './RefreshIndicator'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { readingProgressController } from '../services/readingProgressController'
|
||||
import { writingsController } from '../services/writingsController'
|
||||
import { highlightsController } from '../services/highlightsController'
|
||||
|
||||
interface ProfileProps {
|
||||
relayPool: RelayPool
|
||||
@@ -103,17 +103,16 @@ const Profile: React.FC<ProfileProps> = ({
|
||||
})
|
||||
}, [activeAccount?.pubkey, relayPool, eventStore, refreshTrigger])
|
||||
|
||||
// Background fetch to populate event store (non-blocking)
|
||||
// Background fetch via controllers to populate event store
|
||||
useEffect(() => {
|
||||
if (!pubkey || !relayPool || !eventStore) return
|
||||
|
||||
// Fetch all highlights and writings in background (no limits)
|
||||
const relayUrls = getActiveRelayUrls(relayPool)
|
||||
|
||||
fetchHighlights(relayPool, pubkey, undefined, undefined, false, eventStore)
|
||||
// Start controllers to fetch and populate event store
|
||||
// Controllers handle streaming, deduplication, and storage
|
||||
highlightsController.start({ relayPool, eventStore, pubkey })
|
||||
.catch(err => console.warn('⚠️ [Profile] Failed to fetch highlights:', err))
|
||||
|
||||
fetchBlogPostsFromAuthors(relayPool, [pubkey], relayUrls, undefined, null, eventStore)
|
||||
writingsController.start({ relayPool, eventStore, pubkey, force: refreshTrigger > 0 })
|
||||
.catch(err => console.warn('⚠️ [Profile] Failed to fetch writings:', err))
|
||||
}, [pubkey, relayPool, eventStore, refreshTrigger])
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function ShareTargetHandler({ relayPool }: ShareTargetHandlerProp
|
||||
getActiveRelayUrls(relayPool)
|
||||
)
|
||||
showToast('Bookmark saved!')
|
||||
navigate('/me/links')
|
||||
navigate('/my/links')
|
||||
} catch (err) {
|
||||
console.error('Failed to save shared bookmark:', err)
|
||||
showToast('Failed to save bookmark')
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faRightFromBracket, faUserCircle, faGear, faHome, faPersonHiking } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronRight, faRightFromBracket, faUserCircle, faGear, faHome, faPersonHiking, faHighlighter, faBookmark, faPenToSquare, faLink } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import IconButton from './IconButton'
|
||||
import { faBooks } from '../icons/customIcons'
|
||||
|
||||
interface SidebarHeaderProps {
|
||||
onToggleCollapse: () => void
|
||||
@@ -18,6 +19,8 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
|
||||
const navigate = useNavigate()
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null)
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const getProfileImage = () => {
|
||||
return profile?.picture || null
|
||||
@@ -33,22 +36,93 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
|
||||
|
||||
const profileImage = getProfileImage()
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setShowProfileMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (showProfileMenu) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [showProfileMenu])
|
||||
|
||||
const handleMenuItemClick = (action: () => void) => {
|
||||
setShowProfileMenu(false)
|
||||
action()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sidebar-header-bar">
|
||||
{activeAccount && (
|
||||
<button
|
||||
className="profile-avatar-button"
|
||||
title={getUserDisplayName()}
|
||||
onClick={() => navigate('/me')}
|
||||
aria-label={`Profile: ${getUserDisplayName()}`}
|
||||
>
|
||||
{profileImage ? (
|
||||
<img src={profileImage} alt={getUserDisplayName()} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faUserCircle} />
|
||||
<div className="profile-menu-wrapper" ref={menuRef}>
|
||||
<button
|
||||
className="profile-avatar-button"
|
||||
title={getUserDisplayName()}
|
||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||
aria-label={`Profile: ${getUserDisplayName()}`}
|
||||
>
|
||||
{profileImage ? (
|
||||
<img src={profileImage} alt={getUserDisplayName()} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faUserCircle} />
|
||||
)}
|
||||
</button>
|
||||
{showProfileMenu && (
|
||||
<div className="profile-dropdown-menu">
|
||||
<button
|
||||
className="profile-menu-item"
|
||||
onClick={() => handleMenuItemClick(() => navigate('/my/highlights'))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHighlighter} />
|
||||
<span>My Highlights</span>
|
||||
</button>
|
||||
<button
|
||||
className="profile-menu-item"
|
||||
onClick={() => handleMenuItemClick(() => navigate('/my/bookmarks'))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBookmark} />
|
||||
<span>My Bookmarks</span>
|
||||
</button>
|
||||
<button
|
||||
className="profile-menu-item"
|
||||
onClick={() => handleMenuItemClick(() => navigate('/my/reads'))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBooks} />
|
||||
<span>My Reads</span>
|
||||
</button>
|
||||
<button
|
||||
className="profile-menu-item"
|
||||
onClick={() => handleMenuItemClick(() => navigate('/my/links'))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} />
|
||||
<span>My Links</span>
|
||||
</button>
|
||||
<button
|
||||
className="profile-menu-item"
|
||||
onClick={() => handleMenuItemClick(() => navigate('/my/writings'))}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPenToSquare} />
|
||||
<span>My Writings</span>
|
||||
</button>
|
||||
<div className="profile-menu-separator"></div>
|
||||
<button
|
||||
className="profile-menu-item"
|
||||
onClick={() => handleMenuItemClick(onLogout)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRightFromBracket} />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="sidebar-header-right">
|
||||
<IconButton
|
||||
@@ -72,15 +146,6 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
|
||||
ariaLabel="Explore"
|
||||
variant="ghost"
|
||||
/>
|
||||
{activeAccount && (
|
||||
<IconButton
|
||||
icon={faRightFromBracket}
|
||||
onClick={onLogout}
|
||||
title="Logout"
|
||||
ariaLabel="Logout"
|
||||
variant="ghost"
|
||||
/>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
|
||||
@@ -8,8 +8,6 @@ import { eventToHighlight, sortHighlights } from './highlightEventProcessor'
|
||||
type HighlightsCallback = (highlights: Highlight[]) => void
|
||||
type LoadingCallback = (loading: boolean) => void
|
||||
|
||||
const LAST_SYNCED_KEY = 'highlights_last_synced'
|
||||
|
||||
/**
|
||||
* Shared highlights controller
|
||||
* Manages the user's highlights centrally, similar to bookmarkController
|
||||
@@ -68,37 +66,10 @@ class HighlightsController {
|
||||
this.emitHighlights(this.currentHighlights)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last synced timestamp for incremental loading
|
||||
*/
|
||||
private getLastSyncedAt(pubkey: string): number | null {
|
||||
try {
|
||||
const data = localStorage.getItem(LAST_SYNCED_KEY)
|
||||
if (!data) return null
|
||||
const parsed = JSON.parse(data)
|
||||
return parsed[pubkey] || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last synced timestamp
|
||||
*/
|
||||
private setLastSyncedAt(pubkey: string, timestamp: number): void {
|
||||
try {
|
||||
const data = localStorage.getItem(LAST_SYNCED_KEY)
|
||||
const parsed = data ? JSON.parse(data) : {}
|
||||
parsed[pubkey] = timestamp
|
||||
localStorage.setItem(LAST_SYNCED_KEY, JSON.stringify(parsed))
|
||||
} catch (err) {
|
||||
console.warn('[highlights] Failed to save last synced timestamp:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load highlights for a user
|
||||
* Streams results and stores in event store
|
||||
* Always fetches ALL highlights to ensure completeness
|
||||
*/
|
||||
async start(options: {
|
||||
relayPool: RelayPool
|
||||
@@ -124,15 +95,12 @@ class HighlightsController {
|
||||
const seenIds = new Set<string>()
|
||||
const highlightsMap = new Map<string, Highlight>()
|
||||
|
||||
// Get last synced timestamp for incremental loading
|
||||
const lastSyncedAt = force ? null : this.getLastSyncedAt(pubkey)
|
||||
const filter: { kinds: number[]; authors: string[]; since?: number } = {
|
||||
// Fetch ALL highlights without limits (no since filter)
|
||||
// This ensures we get complete results for profile/my pages
|
||||
const filter = {
|
||||
kinds: [KINDS.Highlights],
|
||||
authors: [pubkey]
|
||||
}
|
||||
if (lastSyncedAt) {
|
||||
filter.since = lastSyncedAt
|
||||
}
|
||||
|
||||
const events = await queryEvents(
|
||||
relayPool,
|
||||
@@ -179,12 +147,6 @@ class HighlightsController {
|
||||
this.lastLoadedPubkey = pubkey
|
||||
this.emitHighlights(sorted)
|
||||
|
||||
// Update last synced timestamp
|
||||
if (sorted.length > 0) {
|
||||
const newestTimestamp = Math.max(...sorted.map(h => h.created_at))
|
||||
this.setLastSyncedAt(pubkey, newestTimestamp)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[highlights] ❌ Failed to load highlights:', error)
|
||||
this.currentHighlights = []
|
||||
|
||||
@@ -10,8 +10,6 @@ const { getArticleTitle, getArticleSummary, getArticleImage, getArticlePublished
|
||||
type WritingsCallback = (posts: BlogPostPreview[]) => void
|
||||
type LoadingCallback = (loading: boolean) => void
|
||||
|
||||
const LAST_SYNCED_KEY = 'writings_last_synced'
|
||||
|
||||
/**
|
||||
* Shared writings controller
|
||||
* Manages the user's nostr-native long-form articles (kind:30023) centrally,
|
||||
@@ -71,34 +69,6 @@ class WritingsController {
|
||||
this.emitWritings(this.currentPosts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last synced timestamp for incremental loading
|
||||
*/
|
||||
private getLastSyncedAt(pubkey: string): number | null {
|
||||
try {
|
||||
const data = localStorage.getItem(LAST_SYNCED_KEY)
|
||||
if (!data) return null
|
||||
const parsed = JSON.parse(data)
|
||||
return parsed[pubkey] || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last synced timestamp
|
||||
*/
|
||||
private setLastSyncedAt(pubkey: string, timestamp: number): void {
|
||||
try {
|
||||
const data = localStorage.getItem(LAST_SYNCED_KEY)
|
||||
const parsed = data ? JSON.parse(data) : {}
|
||||
parsed[pubkey] = timestamp
|
||||
localStorage.setItem(LAST_SYNCED_KEY, JSON.stringify(parsed))
|
||||
} catch (err) {
|
||||
console.warn('[writings] Failed to save last synced timestamp:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert NostrEvent to BlogPostPreview using applesauce Helpers
|
||||
*/
|
||||
@@ -127,6 +97,7 @@ class WritingsController {
|
||||
/**
|
||||
* Load writings for a user (kind:30023)
|
||||
* Streams results and stores in event store
|
||||
* Always fetches ALL writings to ensure completeness
|
||||
*/
|
||||
async start(options: {
|
||||
relayPool: RelayPool
|
||||
@@ -152,15 +123,12 @@ class WritingsController {
|
||||
const seenIds = new Set<string>()
|
||||
const uniqueByReplaceable = new Map<string, BlogPostPreview>()
|
||||
|
||||
// Get last synced timestamp for incremental loading
|
||||
const lastSyncedAt = force ? null : this.getLastSyncedAt(pubkey)
|
||||
const filter: { kinds: number[]; authors: string[]; since?: number } = {
|
||||
// Fetch ALL writings without limits (no since filter)
|
||||
// This ensures we get complete results for profile/my pages
|
||||
const filter = {
|
||||
kinds: [KINDS.BlogPost],
|
||||
authors: [pubkey]
|
||||
}
|
||||
if (lastSyncedAt) {
|
||||
filter.since = lastSyncedAt
|
||||
}
|
||||
|
||||
const events = await queryEvents(
|
||||
relayPool,
|
||||
@@ -221,12 +189,6 @@ class WritingsController {
|
||||
this.lastLoadedPubkey = pubkey
|
||||
this.emitWritings(sorted)
|
||||
|
||||
// Update last synced timestamp
|
||||
if (sorted.length > 0) {
|
||||
const newestTimestamp = Math.max(...sorted.map(p => p.event.created_at))
|
||||
this.setLastSyncedAt(pubkey, newestTimestamp)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[writings] ❌ Failed to load writings:', error)
|
||||
this.currentPosts = []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Me page tabs */
|
||||
/* My page tabs */
|
||||
.me-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -71,7 +71,7 @@
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Align highlight list width with profile card width on /me */
|
||||
/* Align highlight list width with profile card width on /my */
|
||||
.me-highlights-list { padding-left: 0; padding-right: 0; }
|
||||
.explore-header .author-card { max-width: 600px; margin: 0 auto; width: 100%; }
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
text-align: left; /* Override center alignment from .app */
|
||||
}
|
||||
|
||||
/* Bookmark filters in Me page */
|
||||
/* Bookmark filters in My page */
|
||||
.me-tab-content .bookmark-filters {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
@@ -192,7 +192,8 @@
|
||||
}
|
||||
|
||||
.reader-markdown img, .reader-html img {
|
||||
max-width: 100%;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,28 @@
|
||||
|
||||
.highlights-actions { display: flex; align-items: center; justify-content: space-between; width: 100%; }
|
||||
.highlights-actions-left { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.highlights-actions-right { display: flex; align-items: center; gap: 0.5rem; }
|
||||
|
||||
/* Collapse button in highlights header */
|
||||
.highlights-header .toggle-highlights-btn {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.highlights-header .toggle-highlights-btn:hover { background: var(--color-bg-elevated); color: var(--color-text); }
|
||||
.highlights-header .toggle-highlights-btn:active { transform: translateY(1px); }
|
||||
|
||||
.highlights-title { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.highlights-title h3 { margin: 0; font-size: 1rem; font-weight: 600; }
|
||||
|
||||
@@ -132,6 +132,70 @@
|
||||
.profile-avatar-button img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.profile-avatar-button svg { font-size: 1rem; }
|
||||
|
||||
/* Profile menu wrapper */
|
||||
.profile-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Dropdown menu */
|
||||
.profile-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 180px;
|
||||
padding: 0.25rem;
|
||||
z-index: 1000;
|
||||
animation: profileMenuSlideIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes profileMenuSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-menu-item:hover {
|
||||
background: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.profile-menu-item svg {
|
||||
width: 1em;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.profile-menu-separator {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.sidebar-header-bar .toggle-sidebar-btn {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
|
||||
Reference in New Issue
Block a user