Compare commits

...

11 Commits

Author SHA1 Message Date
Gigi
1800ee324e chore: bump version to 0.10.19 2025-10-23 17:01:33 +02:00
Gigi
7d2dac2f1a fix: remove unused props and clean up linting errors
- Remove unused lastFetchTime parameter from BookmarkList
- Remove unused loading and onRefresh parameters from HighlightsPanelHeader
- Update HighlightsPanel to not pass removed props
- All linting and type checking now passing
2025-10-23 17:00:55 +02:00
Gigi
7875f1d0bd refactor: remove refresh button from bookmarks sidebar
Remove the refresh IconButton from bookmarks sidebar and clean up unused imports (faRotate, formatDistanceToNow).
2025-10-23 16:57:56 +02:00
Gigi
d9263e07d1 refactor: remove refresh button from highlights sidebar
Remove the refresh IconButton from highlights panel header as it's no longer needed.
2025-10-23 16:56:51 +02:00
Gigi
9a345a7347 style: match highlights collapse button to bookmarks collapse button
Replace IconButton with native button element and apply same CSS styling as bookmarks collapse button for visual consistency.
2025-10-23 16:56:04 +02:00
Gigi
55d1af3bf9 refactor: move collapse highlights button to left side
Move the collapse highlights panel button from right to left side of the header, making it symmetrical to the bookmarks collapse button. Desktop only (hidden on mobile).
2025-10-23 16:54:08 +02:00
Gigi
feb3134b65 refactor: move grouping toggle to left side next to support button
Move the grouped/chronological toggle button from right/center to the left side, positioned next to the orange heart support button in both BookmarkList and Me components for better UX consistency.
2025-10-23 16:52:24 +02:00
Gigi
7d222e099f refactor: make profile picture trigger dropdown menu
Remove separate three-dot button and make the profile picture itself trigger the dropdown menu. This provides a cleaner, more intuitive UX.
2025-10-23 16:49:54 +02:00
Gigi
59436b5b9e refactor: remove redundant logout button from sidebar header
Remove standalone logout IconButton next to explore since logout is now available in the three-dot profile menu
2025-10-23 16:48:18 +02:00
Gigi
2e08954e83 feat: add three-dot profile menu to sidebar header
Add dropdown menu next to profile picture in bookmarks sidebar with:
- My Highlights
- My Bookmarks
- My Reads
- My Links
- My Writings
- Separator
- Logout

Includes click-outside-to-close functionality and smooth animations.
2025-10-23 16:47:26 +02:00
Gigi
9cb1791a3a docs: update CHANGELOG for v0.10.18 2025-10-23 16:39:07 +02:00
10 changed files with 250 additions and 85 deletions

View File

@@ -7,6 +7,39 @@ 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

View File

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

View File

@@ -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')}

View File

@@ -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}

View File

@@ -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>
)

View File

@@ -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'
@@ -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>
)

View File

@@ -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('/my')}
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}

View File

@@ -192,7 +192,8 @@
}
.reader-markdown img, .reader-html img {
max-width: 100%;
max-width: 100% !important;
width: auto !important;
height: auto;
}
}

View File

@@ -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; }

View File

@@ -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);