mirror of
https://github.com/dergigi/boris.git
synced 2025-12-22 00:54:21 +01:00
feat: implement three-level highlight system
- Add three highlight levels: nostrverse (all), friends (followed), and mine (user's own) - Create contactService to fetch user's follow list from kind 3 events - Add three configurable colors in settings (purple, orange, yellow defaults) - Replace mode switcher with independent toggle buttons for each level - Update highlight rendering to apply level-specific colors using CSS custom properties - Add CSS styles for three-level highlights in both marker and underline modes - Classify highlights dynamically based on user's context and follow list - All three levels can be shown/hidden independently via toggle buttons
This commit is contained in:
@@ -8,6 +8,7 @@ import { Highlight } from '../types/highlights'
|
|||||||
import { BookmarkList } from './BookmarkList'
|
import { BookmarkList } from './BookmarkList'
|
||||||
import { fetchBookmarks } from '../services/bookmarkService'
|
import { fetchBookmarks } from '../services/bookmarkService'
|
||||||
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
|
import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService'
|
||||||
|
import { fetchContacts } from '../services/contactService'
|
||||||
import ContentPanel from './ContentPanel'
|
import ContentPanel from './ContentPanel'
|
||||||
import { HighlightsPanel } from './HighlightsPanel'
|
import { HighlightsPanel } from './HighlightsPanel'
|
||||||
import { ReadableContent } from '../services/readerService'
|
import { ReadableContent } from '../services/readerService'
|
||||||
@@ -16,7 +17,7 @@ import Toast from './Toast'
|
|||||||
import { useSettings } from '../hooks/useSettings'
|
import { useSettings } from '../hooks/useSettings'
|
||||||
import { useArticleLoader } from '../hooks/useArticleLoader'
|
import { useArticleLoader } from '../hooks/useArticleLoader'
|
||||||
import { loadContent, BookmarkReference } from '../utils/contentLoader'
|
import { loadContent, BookmarkReference } from '../utils/contentLoader'
|
||||||
import { HighlightMode } from './HighlightsPanel'
|
import { HighlightVisibility } from './HighlightsPanel'
|
||||||
export type ViewMode = 'compact' | 'cards' | 'large'
|
export type ViewMode = 'compact' | 'cards' | 'large'
|
||||||
|
|
||||||
interface BookmarksProps {
|
interface BookmarksProps {
|
||||||
@@ -40,7 +41,12 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState<string | undefined>(undefined)
|
const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState<string | undefined>(undefined)
|
||||||
const [currentArticleEventId, setCurrentArticleEventId] = useState<string | undefined>(undefined)
|
const [currentArticleEventId, setCurrentArticleEventId] = useState<string | undefined>(undefined)
|
||||||
const [highlightMode, setHighlightMode] = useState<HighlightMode>('others')
|
const [highlightVisibility, setHighlightVisibility] = useState<HighlightVisibility>({
|
||||||
|
nostrverse: true,
|
||||||
|
friends: true,
|
||||||
|
mine: true
|
||||||
|
})
|
||||||
|
const [followedPubkeys, setFollowedPubkeys] = useState<Set<string>>(new Set())
|
||||||
const activeAccount = Hooks.useActiveAccount()
|
const activeAccount = Hooks.useActiveAccount()
|
||||||
const accountManager = Hooks.useAccountManager()
|
const accountManager = Hooks.useAccountManager()
|
||||||
const eventStore = useEventStore()
|
const eventStore = useEventStore()
|
||||||
@@ -72,8 +78,15 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
if (!relayPool || !activeAccount) return
|
if (!relayPool || !activeAccount) return
|
||||||
handleFetchBookmarks()
|
handleFetchBookmarks()
|
||||||
handleFetchHighlights()
|
handleFetchHighlights()
|
||||||
|
handleFetchContacts()
|
||||||
}, [relayPool, activeAccount?.pubkey])
|
}, [relayPool, activeAccount?.pubkey])
|
||||||
|
|
||||||
|
const handleFetchContacts = async () => {
|
||||||
|
if (!relayPool || !activeAccount) return
|
||||||
|
const contacts = await fetchContacts(relayPool, activeAccount.pubkey)
|
||||||
|
setFollowedPubkeys(contacts)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply UI settings
|
// Apply UI settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings.defaultViewMode) setViewMode(settings.defaultViewMode)
|
if (settings.defaultViewMode) setViewMode(settings.defaultViewMode)
|
||||||
@@ -194,8 +207,9 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
onRefresh={handleFetchHighlights}
|
onRefresh={handleFetchHighlights}
|
||||||
onHighlightClick={setSelectedHighlightId}
|
onHighlightClick={setSelectedHighlightId}
|
||||||
currentUserPubkey={activeAccount?.pubkey}
|
currentUserPubkey={activeAccount?.pubkey}
|
||||||
highlightMode={highlightMode}
|
highlightVisibility={highlightVisibility}
|
||||||
onHighlightModeChange={setHighlightMode}
|
onHighlightVisibilityChange={setHighlightVisibility}
|
||||||
|
followedPubkeys={followedPubkeys}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faChevronRight, faHighlighter, faEye, faEyeSlash, faRotate, faUser, faUserGroup } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronRight, faHighlighter, faEye, faEyeSlash, faRotate, faUser, faUserGroup, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Highlight } from '../types/highlights'
|
import { Highlight } from '../types/highlights'
|
||||||
import { HighlightItem } from './HighlightItem'
|
import { HighlightItem } from './HighlightItem'
|
||||||
|
|
||||||
export type HighlightMode = 'mine' | 'others'
|
export interface HighlightVisibility {
|
||||||
|
nostrverse: boolean
|
||||||
|
friends: boolean
|
||||||
|
mine: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface HighlightsPanelProps {
|
interface HighlightsPanelProps {
|
||||||
highlights: Highlight[]
|
highlights: Highlight[]
|
||||||
@@ -18,8 +22,9 @@ interface HighlightsPanelProps {
|
|||||||
onRefresh?: () => void
|
onRefresh?: () => void
|
||||||
onHighlightClick?: (highlightId: string) => void
|
onHighlightClick?: (highlightId: string) => void
|
||||||
currentUserPubkey?: string
|
currentUserPubkey?: string
|
||||||
highlightMode?: HighlightMode
|
highlightVisibility?: HighlightVisibility
|
||||||
onHighlightModeChange?: (mode: HighlightMode) => void
|
onHighlightVisibilityChange?: (visibility: HighlightVisibility) => void
|
||||||
|
followedPubkeys?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
||||||
@@ -34,8 +39,9 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
onHighlightClick,
|
onHighlightClick,
|
||||||
currentUserPubkey,
|
currentUserPubkey,
|
||||||
highlightMode = 'others',
|
highlightVisibility = { nostrverse: true, friends: true, mine: true },
|
||||||
onHighlightModeChange
|
onHighlightVisibilityChange,
|
||||||
|
followedPubkeys = new Set()
|
||||||
}) => {
|
}) => {
|
||||||
const [showUnderlines, setShowUnderlines] = useState(true)
|
const [showUnderlines, setShowUnderlines] = useState(true)
|
||||||
|
|
||||||
@@ -45,7 +51,7 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
|||||||
onToggleUnderlines?.(newValue)
|
onToggleUnderlines?.(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter highlights based on mode and URL
|
// Filter highlights based on visibility levels and URL
|
||||||
const filteredHighlights = useMemo(() => {
|
const filteredHighlights = useMemo(() => {
|
||||||
if (!selectedUrl) return highlights
|
if (!selectedUrl) return highlights
|
||||||
|
|
||||||
@@ -75,18 +81,25 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by mode (mine vs others)
|
// Classify and filter by visibility levels
|
||||||
if (!currentUserPubkey) {
|
return urlFiltered
|
||||||
// If no user is logged in, show all highlights (others mode only makes sense)
|
.map(h => {
|
||||||
return urlFiltered
|
// Classify highlight level
|
||||||
}
|
let level: 'mine' | 'friends' | 'nostrverse' = 'nostrverse'
|
||||||
|
if (h.pubkey === currentUserPubkey) {
|
||||||
if (highlightMode === 'mine') {
|
level = 'mine'
|
||||||
return urlFiltered.filter(h => h.pubkey === currentUserPubkey)
|
} else if (followedPubkeys.has(h.pubkey)) {
|
||||||
} else {
|
level = 'friends'
|
||||||
return urlFiltered.filter(h => h.pubkey !== currentUserPubkey)
|
}
|
||||||
}
|
return { ...h, level }
|
||||||
}, [highlights, selectedUrl, highlightMode, currentUserPubkey])
|
})
|
||||||
|
.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])
|
||||||
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
const hasHighlights = filteredHighlights.length > 0
|
const hasHighlights = filteredHighlights.length > 0
|
||||||
@@ -115,24 +128,46 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
|
|||||||
{!loading && <span className="count">({filteredHighlights.length})</span>}
|
{!loading && <span className="count">({filteredHighlights.length})</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="highlights-actions">
|
<div className="highlights-actions">
|
||||||
{currentUserPubkey && onHighlightModeChange && (
|
{onHighlightVisibilityChange && (
|
||||||
<div className="highlight-mode-toggle">
|
<div className="highlight-level-toggles">
|
||||||
<button
|
<button
|
||||||
onClick={() => onHighlightModeChange('mine')}
|
onClick={() => onHighlightVisibilityChange({
|
||||||
className={`mode-btn ${highlightMode === 'mine' ? 'active' : ''}`}
|
...highlightVisibility,
|
||||||
title="My highlights"
|
nostrverse: !highlightVisibility.nostrverse
|
||||||
aria-label="Show my highlights"
|
})}
|
||||||
|
className={`level-toggle-btn ${highlightVisibility.nostrverse ? 'active' : ''}`}
|
||||||
|
title="Toggle nostrverse highlights"
|
||||||
|
aria-label="Toggle nostrverse highlights"
|
||||||
|
style={{ color: highlightVisibility.nostrverse ? 'var(--highlight-color-nostrverse, #9333ea)' : undefined }}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUser} />
|
<FontAwesomeIcon icon={faGlobe} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onHighlightModeChange('others')}
|
onClick={() => onHighlightVisibilityChange({
|
||||||
className={`mode-btn ${highlightMode === 'others' ? 'active' : ''}`}
|
...highlightVisibility,
|
||||||
title="Other highlights"
|
friends: !highlightVisibility.friends
|
||||||
aria-label="Show highlights from others"
|
})}
|
||||||
|
className={`level-toggle-btn ${highlightVisibility.friends ? 'active' : ''}`}
|
||||||
|
title="Toggle friends highlights"
|
||||||
|
aria-label="Toggle friends highlights"
|
||||||
|
style={{ color: highlightVisibility.friends ? 'var(--highlight-color-friends, #f97316)' : undefined }}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUserGroup} />
|
<FontAwesomeIcon icon={faUserGroup} />
|
||||||
</button>
|
</button>
|
||||||
|
{currentUserPubkey && (
|
||||||
|
<button
|
||||||
|
onClick={() => onHighlightVisibilityChange({
|
||||||
|
...highlightVisibility,
|
||||||
|
mine: !highlightVisibility.mine
|
||||||
|
})}
|
||||||
|
className={`level-toggle-btn ${highlightVisibility.mine ? 'active' : ''}`}
|
||||||
|
title="Toggle my highlights"
|
||||||
|
aria-label="Toggle my highlights"
|
||||||
|
style={{ color: highlightVisibility.mine ? 'var(--highlight-color-mine, #eab308)' : undefined }}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faUser} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
|
|||||||
@@ -122,13 +122,37 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="setting-group setting-inline">
|
<div className="setting-group setting-inline">
|
||||||
<label>Highlight Color</label>
|
<label>Highlight Color (Legacy)</label>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
selectedColor={localSettings.highlightColor || '#ffff00'}
|
selectedColor={localSettings.highlightColor || '#ffff00'}
|
||||||
onColorChange={(color) => setLocalSettings({ ...localSettings, highlightColor: color })}
|
onColorChange={(color) => setLocalSettings({ ...localSettings, highlightColor: color })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-group setting-inline">
|
||||||
|
<label>My Highlights Color</label>
|
||||||
|
<ColorPicker
|
||||||
|
selectedColor={localSettings.highlightColorMine || '#eab308'}
|
||||||
|
onColorChange={(color) => setLocalSettings({ ...localSettings, highlightColorMine: color })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-group setting-inline">
|
||||||
|
<label>Friends Highlights Color</label>
|
||||||
|
<ColorPicker
|
||||||
|
selectedColor={localSettings.highlightColorFriends || '#f97316'}
|
||||||
|
onColorChange={(color) => setLocalSettings({ ...localSettings, highlightColorFriends: color })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setting-group setting-inline">
|
||||||
|
<label>Nostrverse Highlights Color</label>
|
||||||
|
<ColorPicker
|
||||||
|
selectedColor={localSettings.highlightColorNostrverse || '#9333ea'}
|
||||||
|
onColorChange={(color) => setLocalSettings({ ...localSettings, highlightColorNostrverse: color })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="setting-preview">
|
<div className="setting-preview">
|
||||||
<div className="preview-label">Preview</div>
|
<div className="preview-label">Preview</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
|
|||||||
if (fontKey !== 'system') loadFont(fontKey)
|
if (fontKey !== 'system') loadFont(fontKey)
|
||||||
root.setProperty('--reading-font', getFontFamily(fontKey))
|
root.setProperty('--reading-font', getFontFamily(fontKey))
|
||||||
root.setProperty('--reading-font-size', `${settings.fontSize || 16}px`)
|
root.setProperty('--reading-font-size', `${settings.fontSize || 16}px`)
|
||||||
|
|
||||||
|
// Set highlight colors for three levels
|
||||||
|
root.setProperty('--highlight-color-mine', settings.highlightColorMine || '#eab308')
|
||||||
|
root.setProperty('--highlight-color-friends', settings.highlightColorFriends || '#f97316')
|
||||||
|
root.setProperty('--highlight-color-nostrverse', settings.highlightColorNostrverse || '#9333ea')
|
||||||
}, [settings])
|
}, [settings])
|
||||||
|
|
||||||
const saveSettingsWithToast = useCallback(async (newSettings: UserSettings) => {
|
const saveSettingsWithToast = useCallback(async (newSettings: UserSettings) => {
|
||||||
|
|||||||
144
src/index.css
144
src/index.css
@@ -1293,6 +1293,39 @@ body {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Three-level highlight toggles */
|
||||||
|
.highlight-level-toggles {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-level-toggles .level-toggle-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-level-toggles .level-toggle-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-level-toggles .level-toggle-btn.active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-level-toggles .level-toggle-btn:not(.active) {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.refresh-highlights-btn,
|
.refresh-highlights-btn,
|
||||||
.toggle-underlines-btn,
|
.toggle-underlines-btn,
|
||||||
.toggle-highlights-btn {
|
.toggle-highlights-btn {
|
||||||
@@ -1555,6 +1588,68 @@ body {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Three-level highlight colors */
|
||||||
|
.content-highlight-marker.level-mine,
|
||||||
|
.content-highlight.level-mine {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-mine, #eab308) 35%, transparent);
|
||||||
|
box-shadow: 0 0 8px color-mix(in srgb, var(--highlight-color-mine, #eab308) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-mine:hover,
|
||||||
|
.content-highlight.level-mine:hover {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-mine, #eab308) 50%, transparent);
|
||||||
|
box-shadow: 0 0 12px color-mix(in srgb, var(--highlight-color-mine, #eab308) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-friends,
|
||||||
|
.content-highlight.level-friends {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-friends, #f97316) 35%, transparent);
|
||||||
|
box-shadow: 0 0 8px color-mix(in srgb, var(--highlight-color-friends, #f97316) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-friends:hover,
|
||||||
|
.content-highlight.level-friends:hover {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-friends, #f97316) 50%, transparent);
|
||||||
|
box-shadow: 0 0 12px color-mix(in srgb, var(--highlight-color-friends, #f97316) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-nostrverse,
|
||||||
|
.content-highlight.level-nostrverse {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 35%, transparent);
|
||||||
|
box-shadow: 0 0 8px color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-nostrverse:hover,
|
||||||
|
.content-highlight.level-nostrverse:hover {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 50%, transparent);
|
||||||
|
box-shadow: 0 0 12px color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Underline styles for three levels */
|
||||||
|
.content-highlight-underline.level-mine {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--highlight-color-mine, #eab308) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-mine:hover {
|
||||||
|
text-decoration-color: var(--highlight-color-mine, #eab308);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-friends {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--highlight-color-friends, #f97316) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-friends:hover {
|
||||||
|
text-decoration-color: var(--highlight-color-friends, #f97316);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-nostrverse {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-nostrverse:hover {
|
||||||
|
text-decoration-color: var(--highlight-color-nostrverse, #9333ea);
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure highlights work in both light and dark mode */
|
/* Ensure highlights work in both light and dark mode */
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.content-highlight,
|
.content-highlight,
|
||||||
@@ -1577,6 +1672,55 @@ body {
|
|||||||
text-decoration-color: rgba(var(--highlight-rgb, 255, 255, 0), 1);
|
text-decoration-color: rgba(var(--highlight-rgb, 255, 255, 0), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Three-level overrides for light mode */
|
||||||
|
.content-highlight-marker.level-mine,
|
||||||
|
.content-highlight.level-mine {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-mine, #eab308) 40%, transparent);
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--highlight-color-mine, #eab308) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-mine:hover,
|
||||||
|
.content-highlight.level-mine:hover {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-mine, #eab308) 55%, transparent);
|
||||||
|
box-shadow: 0 0 10px color-mix(in srgb, var(--highlight-color-mine, #eab308) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-friends,
|
||||||
|
.content-highlight.level-friends {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-friends, #f97316) 40%, transparent);
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--highlight-color-friends, #f97316) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-friends:hover,
|
||||||
|
.content-highlight.level-friends:hover {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-friends, #f97316) 55%, transparent);
|
||||||
|
box-shadow: 0 0 10px color-mix(in srgb, var(--highlight-color-friends, #f97316) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-nostrverse,
|
||||||
|
.content-highlight.level-nostrverse {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 40%, transparent);
|
||||||
|
box-shadow: 0 0 6px color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-marker.level-nostrverse:hover,
|
||||||
|
.content-highlight.level-nostrverse:hover {
|
||||||
|
background: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 55%, transparent);
|
||||||
|
box-shadow: 0 0 10px color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-mine {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--highlight-color-mine, #eab308) 90%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-friends {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--highlight-color-friends, #f97316) 90%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-highlight-underline.level-nostrverse {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--highlight-color-nostrverse, #9333ea) 90%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.highlight-indicator {
|
.highlight-indicator {
|
||||||
background: rgba(100, 108, 255, 0.15);
|
background: rgba(100, 108, 255, 0.15);
|
||||||
border-color: rgba(100, 108, 255, 0.4);
|
border-color: rgba(100, 108, 255, 0.4);
|
||||||
|
|||||||
51
src/services/contactService.ts
Normal file
51
src/services/contactService.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { RelayPool, completeOnEose } from 'applesauce-relay'
|
||||||
|
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
|
||||||
|
import { NostrEvent } from 'nostr-tools'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the contact list (follows) for a specific user
|
||||||
|
* @param relayPool - The relay pool to query
|
||||||
|
* @param pubkey - The user's public key
|
||||||
|
* @returns Set of pubkeys that the user follows
|
||||||
|
*/
|
||||||
|
export const fetchContacts = async (
|
||||||
|
relayPool: RelayPool,
|
||||||
|
pubkey: string
|
||||||
|
): Promise<Set<string>> => {
|
||||||
|
try {
|
||||||
|
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
|
||||||
|
|
||||||
|
console.log('🔍 Fetching contacts (kind 3) for user:', pubkey)
|
||||||
|
|
||||||
|
const events = await lastValueFrom(
|
||||||
|
relayPool
|
||||||
|
.req(relayUrls, { kinds: [3], authors: [pubkey] })
|
||||||
|
.pipe(completeOnEose(), takeUntil(timer(10000)), toArray())
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('📊 Contact events fetched:', events.length)
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent contact list
|
||||||
|
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
const contactList = sortedEvents[0]
|
||||||
|
|
||||||
|
// Extract pubkeys from 'p' tags
|
||||||
|
const followedPubkeys = new Set<string>()
|
||||||
|
for (const tag of contactList.tags) {
|
||||||
|
if (tag[0] === 'p' && tag[1]) {
|
||||||
|
followedPubkeys.add(tag[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('👥 Followed contacts:', followedPubkeys.size)
|
||||||
|
|
||||||
|
return followedPubkeys
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch contacts:', error)
|
||||||
|
return new Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ export interface UserSettings {
|
|||||||
fontSize?: number
|
fontSize?: number
|
||||||
highlightStyle?: 'marker' | 'underline'
|
highlightStyle?: 'marker' | 'underline'
|
||||||
highlightColor?: string
|
highlightColor?: string
|
||||||
|
// Three-level highlight colors
|
||||||
|
highlightColorNostrverse?: string
|
||||||
|
highlightColorFriends?: string
|
||||||
|
highlightColorMine?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSettings(
|
export async function loadSettings(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// NIP-84 Highlight types
|
// NIP-84 Highlight types
|
||||||
|
export type HighlightLevel = 'nostrverse' | 'friends' | 'mine'
|
||||||
|
|
||||||
export interface Highlight {
|
export interface Highlight {
|
||||||
id: string
|
id: string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -11,5 +13,7 @@ export interface Highlight {
|
|||||||
author?: string // 'p' tag with 'author' role
|
author?: string // 'p' tag with 'author' role
|
||||||
context?: string // surrounding text context
|
context?: string // surrounding text context
|
||||||
comment?: string // optional comment about the highlight
|
comment?: string // optional comment about the highlight
|
||||||
|
// Level classification (computed based on user's context)
|
||||||
|
level?: HighlightLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,11 +73,13 @@ export function applyHighlightsToText(
|
|||||||
|
|
||||||
// Add the highlighted text
|
// Add the highlighted text
|
||||||
const highlightedText = text.substring(match.startIndex, match.endIndex)
|
const highlightedText = text.substring(match.startIndex, match.endIndex)
|
||||||
|
const levelClass = match.highlight.level ? ` level-${match.highlight.level}` : ''
|
||||||
result.push(
|
result.push(
|
||||||
<mark
|
<mark
|
||||||
key={`highlight-${match.highlight.id}-${match.startIndex}`}
|
key={`highlight-${match.highlight.id}-${match.startIndex}`}
|
||||||
className="content-highlight"
|
className={`content-highlight${levelClass}`}
|
||||||
data-highlight-id={match.highlight.id}
|
data-highlight-id={match.highlight.id}
|
||||||
|
data-highlight-level={match.highlight.level || 'nostrverse'}
|
||||||
title={`Highlighted ${new Date(match.highlight.created_at * 1000).toLocaleDateString()}`}
|
title={`Highlighted ${new Date(match.highlight.created_at * 1000).toLocaleDateString()}`}
|
||||||
>
|
>
|
||||||
{highlightedText}
|
{highlightedText}
|
||||||
@@ -101,8 +103,10 @@ const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim()
|
|||||||
// Helper to create a mark element for a highlight
|
// Helper to create a mark element for a highlight
|
||||||
function createMarkElement(highlight: Highlight, matchText: string, highlightStyle: 'marker' | 'underline' = 'marker'): HTMLElement {
|
function createMarkElement(highlight: Highlight, matchText: string, highlightStyle: 'marker' | 'underline' = 'marker'): HTMLElement {
|
||||||
const mark = document.createElement('mark')
|
const mark = document.createElement('mark')
|
||||||
mark.className = `content-highlight-${highlightStyle}`
|
const levelClass = highlight.level ? ` level-${highlight.level}` : ''
|
||||||
|
mark.className = `content-highlight-${highlightStyle}${levelClass}`
|
||||||
mark.setAttribute('data-highlight-id', highlight.id)
|
mark.setAttribute('data-highlight-id', highlight.id)
|
||||||
|
mark.setAttribute('data-highlight-level', highlight.level || 'nostrverse')
|
||||||
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
|
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
|
||||||
mark.textContent = matchText
|
mark.textContent = matchText
|
||||||
return mark
|
return mark
|
||||||
|
|||||||
Reference in New Issue
Block a user