From 9ae918f7443baed8231fa5f234c1e04e7a335b42 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 7 Oct 2025 21:53:24 +0100 Subject: [PATCH] refactor(highlights): extract highlights panel components - Create useFilteredHighlights hook for highlight filtering - Extract HighlightsPanelCollapsed component - Extract HighlightsPanelHeader component - Reduce HighlightsPanel.tsx from 232 lines to 118 lines --- src/components/HighlightsPanel.tsx | 168 +++--------------- .../HighlightsPanelCollapsed.tsx | 30 ++++ .../HighlightsPanel/HighlightsPanelHeader.tsx | 111 ++++++++++++ src/hooks/useFilteredHighlights.ts | 68 +++++++ 4 files changed, 236 insertions(+), 141 deletions(-) create mode 100644 src/components/HighlightsPanel/HighlightsPanelCollapsed.tsx create mode 100644 src/components/HighlightsPanel/HighlightsPanelHeader.tsx create mode 100644 src/hooks/useFilteredHighlights.ts diff --git a/src/components/HighlightsPanel.tsx b/src/components/HighlightsPanel.tsx index e3f78523..a4a910ab 100644 --- a/src/components/HighlightsPanel.tsx +++ b/src/components/HighlightsPanel.tsx @@ -1,8 +1,11 @@ -import React, { useMemo, useState } from 'react' +import React, { useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faChevronRight, faHighlighter, faEye, faEyeSlash, faRotate, faUser, faUserGroup, faNetworkWired } from '@fortawesome/free-solid-svg-icons' +import { faHighlighter } from '@fortawesome/free-solid-svg-icons' import { Highlight } from '../types/highlights' import { HighlightItem } from './HighlightItem' +import { useFilteredHighlights } from '../hooks/useFilteredHighlights' +import HighlightsPanelCollapsed from './HighlightsPanel/HighlightsPanelCollapsed' +import HighlightsPanelHeader from './HighlightsPanel/HighlightsPanelHeader' export interface HighlightVisibility { nostrverse: boolean @@ -51,153 +54,36 @@ export const HighlightsPanel: React.FC = ({ onToggleHighlights?.(newValue) } - // Filter highlights based on visibility levels and URL - const filteredHighlights = useMemo(() => { - if (!selectedUrl) return highlights - - 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:')) { - // 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) - }) - } - - // Classify and filter by visibility levels - return urlFiltered - .map(h => { - // Classify highlight level - let level: 'mine' | 'friends' | 'nostrverse' = 'nostrverse' - if (h.pubkey === currentUserPubkey) { - level = 'mine' - } else if (followedPubkeys.has(h.pubkey)) { - level = 'friends' - } - return { ...h, level } - }) - .filter(h => { - // Filter by visibility settings - if (h.level === 'mine') return highlightVisibility.mine - if (h.level === 'friends') return highlightVisibility.friends - return highlightVisibility.nostrverse - }) - }, [highlights, selectedUrl, highlightVisibility, currentUserPubkey, followedPubkeys]) + const filteredHighlights = useFilteredHighlights({ + highlights, + selectedUrl, + highlightVisibility, + currentUserPubkey, + followedPubkeys + }) if (isCollapsed) { - const hasHighlights = filteredHighlights.length > 0 - return ( -
- -
+ 0} + onToggleCollapse={onToggleCollapse} + /> ) } return (
-
-
-
- {onHighlightVisibilityChange && ( -
- - - -
- )} - {onRefresh && ( - - )} - {filteredHighlights.length > 0 && ( - - )} -
- -
-
+ 0} + showHighlights={showHighlights} + highlightVisibility={highlightVisibility} + currentUserPubkey={currentUserPubkey} + onToggleHighlights={handleToggleHighlights} + onRefresh={onRefresh} + onToggleCollapse={onToggleCollapse} + onHighlightVisibilityChange={onHighlightVisibilityChange} + /> {loading && filteredHighlights.length === 0 ? (
diff --git a/src/components/HighlightsPanel/HighlightsPanelCollapsed.tsx b/src/components/HighlightsPanel/HighlightsPanelCollapsed.tsx new file mode 100644 index 00000000..00c84158 --- /dev/null +++ b/src/components/HighlightsPanel/HighlightsPanelCollapsed.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faHighlighter, faChevronRight } from '@fortawesome/free-solid-svg-icons' + +interface HighlightsPanelCollapsedProps { + hasHighlights: boolean + onToggleCollapse: () => void +} + +const HighlightsPanelCollapsed: React.FC = ({ + hasHighlights, + onToggleCollapse +}) => { + return ( +
+ +
+ ) +} + +export default HighlightsPanelCollapsed + diff --git a/src/components/HighlightsPanel/HighlightsPanelHeader.tsx b/src/components/HighlightsPanel/HighlightsPanelHeader.tsx new file mode 100644 index 00000000..2faffc05 --- /dev/null +++ b/src/components/HighlightsPanel/HighlightsPanelHeader.tsx @@ -0,0 +1,111 @@ +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faChevronRight, faEye, faEyeSlash, faRotate, faUser, faUserGroup, faNetworkWired } from '@fortawesome/free-solid-svg-icons' +import { HighlightVisibility } from '../HighlightsPanel' + +interface HighlightsPanelHeaderProps { + loading: boolean + hasHighlights: boolean + showHighlights: boolean + highlightVisibility: HighlightVisibility + currentUserPubkey?: string + onToggleHighlights: () => void + onRefresh?: () => void + onToggleCollapse: () => void + onHighlightVisibilityChange?: (visibility: HighlightVisibility) => void +} + +const HighlightsPanelHeader: React.FC = ({ + loading, + hasHighlights, + showHighlights, + highlightVisibility, + currentUserPubkey, + onToggleHighlights, + onRefresh, + onToggleCollapse, + onHighlightVisibilityChange +}) => { + return ( +
+
+
+ {onHighlightVisibilityChange && ( +
+ + + +
+ )} + {onRefresh && ( + + )} + {hasHighlights && ( + + )} +
+ +
+
+ ) +} + +export default HighlightsPanelHeader + diff --git a/src/hooks/useFilteredHighlights.ts b/src/hooks/useFilteredHighlights.ts new file mode 100644 index 00000000..b224f939 --- /dev/null +++ b/src/hooks/useFilteredHighlights.ts @@ -0,0 +1,68 @@ +import { useMemo } from 'react' +import { Highlight } from '../types/highlights' +import { HighlightVisibility } from '../components/HighlightsPanel' + +/** + * Normalize URL for comparison + */ +function normalizeUrl(url: string): 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() + } +} + +interface UseFilteredHighlightsParams { + highlights: Highlight[] + selectedUrl?: string + highlightVisibility: HighlightVisibility + currentUserPubkey?: string + followedPubkeys: Set +} + +export const useFilteredHighlights = ({ + highlights, + selectedUrl, + highlightVisibility, + currentUserPubkey, + followedPubkeys +}: UseFilteredHighlightsParams) => { + return useMemo(() => { + if (!selectedUrl) return highlights + + let urlFiltered = highlights + + // For Nostr articles, we already fetched highlights specifically for this article + if (!selectedUrl.startsWith('nostr:')) { + 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) + }) + } + + // Classify and filter by visibility levels + return urlFiltered + .map(h => { + let level: 'mine' | 'friends' | 'nostrverse' = 'nostrverse' + if (h.pubkey === currentUserPubkey) { + level = 'mine' + } else if (followedPubkeys.has(h.pubkey)) { + level = 'friends' + } + return { ...h, level } + }) + .filter(h => { + if (h.level === 'mine') return highlightVisibility.mine + if (h.level === 'friends') return highlightVisibility.friends + return highlightVisibility.nostrverse + }) + }, [highlights, selectedUrl, highlightVisibility, currentUserPubkey, followedPubkeys]) +} +