mirror of
https://github.com/dergigi/boris.git
synced 2026-02-11 18:14:31 +01:00
feat: show highlights in article content and add mode toggle
Fixes: - Fixed highlight filtering for Nostr articles in urlHelpers.ts Now returns all highlights for nostr: URLs since they're pre-filtered - This fixes highlights not appearing in article content Features: - Added highlight mode toggle: 'my highlights' vs 'other highlights' - Icons: faUser (mine) and faUserGroup (others) - Mode toggle only shows when user is logged in - Filters highlights by user pubkey based on selected mode - Default mode is 'others' to show community highlights - Added CSS styling for mode toggle buttons Result: Highlights now show both in the panel AND underlined in the article text. Users can switch between viewing their own highlights vs highlights from others.
This commit is contained in:
@@ -16,6 +16,7 @@ import Toast from './Toast'
|
||||
import { useSettings } from '../hooks/useSettings'
|
||||
import { useArticleLoader } from '../hooks/useArticleLoader'
|
||||
import { loadContent, BookmarkReference } from '../utils/contentLoader'
|
||||
import { HighlightMode } from './HighlightsPanel'
|
||||
export type ViewMode = 'compact' | 'cards' | 'large'
|
||||
|
||||
interface BookmarksProps {
|
||||
@@ -39,6 +40,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState<string | undefined>(undefined)
|
||||
const [currentArticleEventId, setCurrentArticleEventId] = useState<string | undefined>(undefined)
|
||||
const [highlightMode, setHighlightMode] = useState<HighlightMode>('others')
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
const eventStore = useEventStore()
|
||||
@@ -191,6 +193,9 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
selectedHighlightId={selectedHighlightId}
|
||||
onRefresh={handleFetchHighlights}
|
||||
onHighlightClick={setSelectedHighlightId}
|
||||
currentUserPubkey={activeAccount?.pubkey}
|
||||
highlightMode={highlightMode}
|
||||
onHighlightModeChange={setHighlightMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faHighlighter, faEye, faEyeSlash, faRotate } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronRight, faHighlighter, faEye, faEyeSlash, faRotate, faUser, faUserGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { HighlightItem } from './HighlightItem'
|
||||
|
||||
export type HighlightMode = 'mine' | 'others'
|
||||
|
||||
interface HighlightsPanelProps {
|
||||
highlights: Highlight[]
|
||||
loading: boolean
|
||||
@@ -15,6 +17,9 @@ interface HighlightsPanelProps {
|
||||
selectedHighlightId?: string
|
||||
onRefresh?: () => void
|
||||
onHighlightClick?: (highlightId: string) => void
|
||||
currentUserPubkey?: string
|
||||
highlightMode?: HighlightMode
|
||||
onHighlightModeChange?: (mode: HighlightMode) => void
|
||||
}
|
||||
|
||||
export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
@@ -27,7 +32,10 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
onToggleUnderlines,
|
||||
selectedHighlightId,
|
||||
onRefresh,
|
||||
onHighlightClick
|
||||
onHighlightClick,
|
||||
currentUserPubkey,
|
||||
highlightMode = 'others',
|
||||
onHighlightModeChange
|
||||
}) => {
|
||||
const [showUnderlines, setShowUnderlines] = useState(true)
|
||||
|
||||
@@ -37,36 +45,48 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
onToggleUnderlines?.(newValue)
|
||||
}
|
||||
|
||||
// Filter highlights to show only those relevant to the current URL or article
|
||||
// Filter highlights based on mode and URL
|
||||
const filteredHighlights = useMemo(() => {
|
||||
if (!selectedUrl) return highlights
|
||||
|
||||
// For Nostr articles (URL starts with "nostr:"), we don't need to filter
|
||||
let urlFiltered = highlights
|
||||
|
||||
// For Nostr articles (URL starts with "nostr:"), we don't need to filter by URL
|
||||
// because we already fetched highlights specifically for this article
|
||||
if (selectedUrl.startsWith('nostr:')) {
|
||||
return highlights
|
||||
}
|
||||
|
||||
// For web URLs, filter by URL matching
|
||||
const normalizeUrl = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith('http') ? url : `https://${url}`)
|
||||
return `${urlObj.hostname.replace(/^www\./, '')}${urlObj.pathname}`.replace(/\/$/, '').toLowerCase()
|
||||
} catch {
|
||||
return url.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/$/, '').toLowerCase()
|
||||
if (!selectedUrl.startsWith('nostr:')) {
|
||||
// For web URLs, filter by URL matching
|
||||
const normalizeUrl = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith('http') ? url : `https://${url}`)
|
||||
return `${urlObj.hostname.replace(/^www\./, '')}${urlObj.pathname}`.replace(/\/$/, '').toLowerCase()
|
||||
} catch {
|
||||
return url.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/$/, '').toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedSelected = normalizeUrl(selectedUrl)
|
||||
|
||||
urlFiltered = highlights.filter(h => {
|
||||
if (!h.urlReference) return false
|
||||
const normalizedRef = normalizeUrl(h.urlReference)
|
||||
return normalizedSelected === normalizedRef ||
|
||||
normalizedSelected.includes(normalizedRef) ||
|
||||
normalizedRef.includes(normalizedSelected)
|
||||
})
|
||||
}
|
||||
|
||||
const normalizedSelected = normalizeUrl(selectedUrl)
|
||||
// Filter by mode (mine vs others)
|
||||
if (!currentUserPubkey) {
|
||||
// If no user is logged in, show all highlights (others mode only makes sense)
|
||||
return urlFiltered
|
||||
}
|
||||
|
||||
return highlights.filter(h => {
|
||||
if (!h.urlReference) return false
|
||||
const normalizedRef = normalizeUrl(h.urlReference)
|
||||
return normalizedSelected === normalizedRef ||
|
||||
normalizedSelected.includes(normalizedRef) ||
|
||||
normalizedRef.includes(normalizedSelected)
|
||||
})
|
||||
}, [highlights, selectedUrl])
|
||||
if (highlightMode === 'mine') {
|
||||
return urlFiltered.filter(h => h.pubkey === currentUserPubkey)
|
||||
} else {
|
||||
return urlFiltered.filter(h => h.pubkey !== currentUserPubkey)
|
||||
}
|
||||
}, [highlights, selectedUrl, highlightMode, currentUserPubkey])
|
||||
|
||||
if (isCollapsed) {
|
||||
const hasHighlights = filteredHighlights.length > 0
|
||||
@@ -95,6 +115,26 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||
{!loading && <span className="count">({filteredHighlights.length})</span>}
|
||||
</div>
|
||||
<div className="highlights-actions">
|
||||
{currentUserPubkey && onHighlightModeChange && (
|
||||
<div className="highlight-mode-toggle">
|
||||
<button
|
||||
onClick={() => onHighlightModeChange('mine')}
|
||||
className={`mode-btn ${highlightMode === 'mine' ? 'active' : ''}`}
|
||||
title="My highlights"
|
||||
aria-label="Show my highlights"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onHighlightModeChange('others')}
|
||||
className={`mode-btn ${highlightMode === 'others' ? 'active' : ''}`}
|
||||
title="Other highlights"
|
||||
aria-label="Show highlights from others"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserGroup} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
|
||||
@@ -1260,6 +1260,35 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.highlight-mode-toggle {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.highlight-mode-toggle .mode-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.highlight-mode-toggle .mode-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.highlight-mode-toggle .mode-btn.active {
|
||||
background: #646cff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.refresh-highlights-btn,
|
||||
.toggle-underlines-btn,
|
||||
.toggle-highlights-btn {
|
||||
|
||||
@@ -12,6 +12,13 @@ export function normalizeUrl(url: string): string {
|
||||
export function filterHighlightsByUrl(highlights: Highlight[], selectedUrl: string | undefined): Highlight[] {
|
||||
if (!selectedUrl || highlights.length === 0) return []
|
||||
|
||||
// For Nostr articles, we already fetched highlights specifically for this article
|
||||
// So we don't need to filter them - they're all relevant
|
||||
if (selectedUrl.startsWith('nostr:')) {
|
||||
return highlights
|
||||
}
|
||||
|
||||
// For web URLs, filter by URL matching
|
||||
const normalizedSelected = normalizeUrl(selectedUrl)
|
||||
|
||||
return highlights.filter(h => {
|
||||
|
||||
Reference in New Issue
Block a user