mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 12:34:41 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea03c9042 | ||
|
|
4720416f2c | ||
|
|
8ad9e652fb | ||
|
|
98c72389e2 | ||
|
|
e032f432dd | ||
|
|
852465bee7 | ||
|
|
39d0147cfa | ||
|
|
10cc7ce9b0 | ||
|
|
6b8442ebdd | ||
|
|
5aba283e92 | ||
|
|
59df232e2e | ||
|
|
702c001d46 | ||
|
|
48a9919db8 | ||
|
|
d6d0755b89 | ||
|
|
facdd36145 | ||
|
|
5d379a280b | ||
|
|
22a02d228d | ||
|
|
61fd5bbadc | ||
|
|
d642c87527 | ||
|
|
fea425b5d0 | ||
|
|
1609c6e580 | ||
|
|
270ea94c70 | ||
|
|
83e2f23357 | ||
|
|
9df0261071 | ||
|
|
1dfe66651a | ||
|
|
dcb7933ede |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.10",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"homepage": "https://xn--bris-v0b.com/",
|
||||
"homepage": "https://read.withboris.com/",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -52,6 +52,15 @@ function AppRoutes({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
||||
</Routes>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useParams, useLocation } from 'react-router-dom'
|
||||
import React, { useMemo, useEffect, useRef } from 'react'
|
||||
import { useParams, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { useEventStore } from 'applesauce-react/hooks'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
@@ -23,10 +23,21 @@ interface BookmarksProps {
|
||||
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
const { naddr } = useParams<{ naddr?: string }>()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const previousLocationRef = useRef<string>()
|
||||
|
||||
const externalUrl = location.pathname.startsWith('/r/')
|
||||
? decodeURIComponent(location.pathname.slice(3))
|
||||
: undefined
|
||||
|
||||
const showSettings = location.pathname === '/settings'
|
||||
|
||||
// Track previous location for going back from settings
|
||||
useEffect(() => {
|
||||
if (!showSettings) {
|
||||
previousLocationRef.current = location.pathname
|
||||
}
|
||||
}, [location.pathname, showSettings])
|
||||
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
@@ -50,8 +61,6 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
setShowHighlights,
|
||||
selectedHighlightId,
|
||||
setSelectedHighlightId,
|
||||
showSettings,
|
||||
setShowSettings,
|
||||
currentArticleCoordinate,
|
||||
setCurrentArticleCoordinate,
|
||||
currentArticleEventId,
|
||||
@@ -94,7 +103,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
relayPool,
|
||||
settings,
|
||||
setIsCollapsed,
|
||||
setShowSettings,
|
||||
setShowSettings: () => {}, // No-op since we use route-based settings now
|
||||
setCurrentArticle
|
||||
})
|
||||
|
||||
@@ -160,7 +169,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
onLogout={onLogout}
|
||||
onViewModeChange={setViewMode}
|
||||
onOpenSettings={() => {
|
||||
setShowSettings(true)
|
||||
navigate('/settings')
|
||||
setIsCollapsed(true)
|
||||
setIsHighlightsCollapsed(true)
|
||||
}}
|
||||
@@ -171,7 +180,11 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
selectedUrl={selectedUrl}
|
||||
settings={settings}
|
||||
onSaveSettings={saveSettings}
|
||||
onCloseSettings={() => setShowSettings(false)}
|
||||
onCloseSettings={() => {
|
||||
// Navigate back to previous location or default
|
||||
const backTo = previousLocationRef.current || '/'
|
||||
navigate(backTo)
|
||||
}}
|
||||
classifiedHighlights={classifiedHighlights}
|
||||
showHighlights={showHighlights}
|
||||
selectedHighlightId={selectedHighlightId}
|
||||
|
||||
@@ -19,6 +19,8 @@ interface ContentPanelProps {
|
||||
markdown?: string
|
||||
selectedUrl?: string
|
||||
image?: string
|
||||
summary?: string
|
||||
published?: number
|
||||
highlights?: Highlight[]
|
||||
showHighlights?: boolean
|
||||
highlightStyle?: 'marker' | 'underline'
|
||||
@@ -40,6 +42,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
markdown,
|
||||
selectedUrl,
|
||||
image,
|
||||
summary,
|
||||
published,
|
||||
highlights = [],
|
||||
showHighlights = true,
|
||||
highlightStyle = 'marker',
|
||||
@@ -117,6 +121,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
<ReaderHeader
|
||||
title={title}
|
||||
image={image}
|
||||
summary={summary}
|
||||
published={published}
|
||||
readingTimeText={readingStats ? readingStats.text : null}
|
||||
hasHighlights={hasHighlights}
|
||||
highlightCount={relevantHighlights.length}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faHighlighter, faClock } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faHighlighter, faClock, faCalendar } from '@fortawesome/free-solid-svg-icons'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
interface ReaderHeaderProps {
|
||||
title?: string
|
||||
image?: string
|
||||
summary?: string
|
||||
published?: number
|
||||
readingTimeText?: string | null
|
||||
hasHighlights: boolean
|
||||
highlightCount: number
|
||||
@@ -13,21 +16,60 @@ interface ReaderHeaderProps {
|
||||
const ReaderHeader: React.FC<ReaderHeaderProps> = ({
|
||||
title,
|
||||
image,
|
||||
summary,
|
||||
published,
|
||||
readingTimeText,
|
||||
hasHighlights,
|
||||
highlightCount
|
||||
}) => {
|
||||
const formattedDate = published ? format(new Date(published * 1000), 'MMMM d, yyyy') : null
|
||||
if (image) {
|
||||
return (
|
||||
<div className="reader-hero-image">
|
||||
<img src={image} alt={title || 'Article image'} />
|
||||
{title && (
|
||||
<div className="reader-header-overlay">
|
||||
<h2 className="reader-title">{title}</h2>
|
||||
{summary && <p className="reader-summary">{summary}</p>}
|
||||
<div className="reader-meta">
|
||||
{formattedDate && (
|
||||
<div className="publish-date">
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
)}
|
||||
{readingTimeText && (
|
||||
<div className="reading-time">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
<span>{readingTimeText}</span>
|
||||
</div>
|
||||
)}
|
||||
{hasHighlights && (
|
||||
<div className="highlight-indicator">
|
||||
<FontAwesomeIcon icon={faHighlighter} />
|
||||
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{image && (
|
||||
<div className="reader-hero-image">
|
||||
<img src={image} alt={title || 'Article image'} />
|
||||
</div>
|
||||
)}
|
||||
{title && (
|
||||
<div className="reader-header">
|
||||
<h2 className="reader-title">{title}</h2>
|
||||
{summary && <p className="reader-summary">{summary}</p>}
|
||||
<div className="reader-meta">
|
||||
{formattedDate && (
|
||||
<div className="publish-date">
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
)}
|
||||
{readingTimeText && (
|
||||
<div className="reading-time">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { faTimes, faUndo } from '@fortawesome/free-solid-svg-icons'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
import IconButton from './IconButton'
|
||||
import { loadFont } from '../utils/fontLoader'
|
||||
@@ -7,6 +8,8 @@ import ReadingDisplaySettings from './Settings/ReadingDisplaySettings'
|
||||
import LayoutNavigationSettings from './Settings/LayoutNavigationSettings'
|
||||
import StartupPreferencesSettings from './Settings/StartupPreferencesSettings'
|
||||
import ZapSettings from './Settings/ZapSettings'
|
||||
import RelaySettings from './Settings/RelaySettings'
|
||||
import { useRelayStatus } from '../hooks/useRelayStatus'
|
||||
|
||||
const DEFAULT_SETTINGS: UserSettings = {
|
||||
collapseOnArticleOpen: true,
|
||||
@@ -33,9 +36,10 @@ interface SettingsProps {
|
||||
settings: UserSettings
|
||||
onSave: (settings: UserSettings) => Promise<void>
|
||||
onClose: () => void
|
||||
relayPool: RelayPool | null
|
||||
}
|
||||
|
||||
const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
||||
const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose, relayPool }) => {
|
||||
const [localSettings, setLocalSettings] = useState<UserSettings>(() => {
|
||||
// Migrate old settings format to new weight-based format
|
||||
const migrated = { ...settings }
|
||||
@@ -52,6 +56,8 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
||||
const isInitialMount = useRef(true)
|
||||
const saveTimeoutRef = useRef<number | null>(null)
|
||||
const isLocallyUpdating = useRef(false)
|
||||
|
||||
const relayStatuses = useRelayStatus({ relayPool })
|
||||
|
||||
useEffect(() => {
|
||||
// Don't update from external settings if we're currently making local changes
|
||||
@@ -152,6 +158,7 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
||||
<LayoutNavigationSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<StartupPreferencesSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<ZapSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||
<RelaySettings relayStatuses={relayStatuses} onClose={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ const LayoutNavigationSettings: React.FC<LayoutNavigationSettingsProps> = ({ set
|
||||
<h3 className="section-title">Layout & Navigation</h3>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Default View Mode</label>
|
||||
<label>Default Bookmark View</label>
|
||||
<div className="setting-buttons">
|
||||
<IconButton
|
||||
icon={faList}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { faHighlighter, faUnderline } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faHighlighter, faUnderline, faNetworkWired, faUserGroup, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
import IconButton from '../IconButton'
|
||||
import ColorPicker from '../ColorPicker'
|
||||
@@ -21,10 +22,12 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label htmlFor="readingFont">Reading Font</label>
|
||||
<FontSelector
|
||||
value={settings.readingFont || 'source-serif-4'}
|
||||
onChange={(font) => onUpdate({ readingFont: font })}
|
||||
/>
|
||||
<div className="setting-control">
|
||||
<FontSelector
|
||||
value={settings.readingFont || 'source-serif-4'}
|
||||
onChange={(font) => onUpdate({ readingFont: font })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
@@ -44,19 +47,6 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-group">
|
||||
<label htmlFor="showHighlights" className="checkbox-label">
|
||||
<input
|
||||
id="showHighlights"
|
||||
type="checkbox"
|
||||
checked={settings.showHighlights !== false}
|
||||
onChange={(e) => onUpdate({ showHighlights: e.target.checked })}
|
||||
className="setting-checkbox"
|
||||
/>
|
||||
<span>Show highlights</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Highlight Style</label>
|
||||
<div className="setting-buttons">
|
||||
@@ -107,6 +97,52 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Default Highlight Visibility</label>
|
||||
<div className="highlight-level-toggles">
|
||||
<button
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityNostrverse: !(settings.defaultHighlightVisibilityNostrverse !== false) })}
|
||||
className={`level-toggle-btn ${(settings.defaultHighlightVisibilityNostrverse !== false) ? 'active' : ''}`}
|
||||
title="Nostrverse highlights"
|
||||
aria-label="Toggle nostrverse highlights by default"
|
||||
style={{ color: (settings.defaultHighlightVisibilityNostrverse !== false) ? 'var(--highlight-color-nostrverse, #9333ea)' : undefined }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faNetworkWired} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityFriends: !(settings.defaultHighlightVisibilityFriends !== false) })}
|
||||
className={`level-toggle-btn ${(settings.defaultHighlightVisibilityFriends !== false) ? 'active' : ''}`}
|
||||
title="Friends highlights"
|
||||
aria-label="Toggle friends highlights by default"
|
||||
style={{ color: (settings.defaultHighlightVisibilityFriends !== false) ? 'var(--highlight-color-friends, #f97316)' : undefined }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserGroup} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityMine: !(settings.defaultHighlightVisibilityMine !== false) })}
|
||||
className={`level-toggle-btn ${(settings.defaultHighlightVisibilityMine !== false) ? 'active' : ''}`}
|
||||
title="My highlights"
|
||||
aria-label="Toggle my highlights by default"
|
||||
style={{ color: (settings.defaultHighlightVisibilityMine !== false) ? 'var(--highlight-color-mine, #eab308)' : undefined }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-group">
|
||||
<label htmlFor="showHighlights" className="checkbox-label">
|
||||
<input
|
||||
id="showHighlights"
|
||||
type="checkbox"
|
||||
checked={settings.showHighlights !== false}
|
||||
onChange={(e) => onUpdate({ showHighlights: e.target.checked })}
|
||||
className="setting-checkbox"
|
||||
/>
|
||||
<span>Show highlights</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="setting-preview">
|
||||
<div className="preview-label">Preview</div>
|
||||
<div
|
||||
@@ -118,9 +154,9 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<h3>The Quick Brown Fox</h3>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <span className={settings.showHighlights !== false ? `content-highlight-${settings.highlightStyle || 'marker'} level-mine` : ""}>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span> Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <span className={settings.showHighlights !== false ? `content-highlight-${settings.highlightStyle || 'marker'} level-friends` : ""}>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</span> Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.</p>
|
||||
<p>Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. <span className={settings.showHighlights !== false ? `content-highlight-${settings.highlightStyle || 'marker'} level-nostrverse` : ""}>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</span> Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <span className={settings.showHighlights !== false && settings.defaultHighlightVisibilityMine !== false ? `content-highlight-${settings.highlightStyle || 'marker'} level-mine` : ""}>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span> Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <span className={settings.showHighlights !== false && settings.defaultHighlightVisibilityFriends !== false ? `content-highlight-${settings.highlightStyle || 'marker'} level-friends` : ""}>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</span> Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.</p>
|
||||
<p>Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. <span className={settings.showHighlights !== false && settings.defaultHighlightVisibilityNostrverse !== false ? `content-highlight-${settings.highlightStyle || 'marker'} level-nostrverse` : ""}>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</span> Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.</p>
|
||||
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
204
src/components/Settings/RelaySettings.tsx
Normal file
204
src/components/Settings/RelaySettings.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCheckCircle, faCircle, faClock } from '@fortawesome/free-solid-svg-icons'
|
||||
import { RelayStatus } from '../../services/relayStatusService'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
|
||||
interface RelaySettingsProps {
|
||||
relayStatuses: RelayStatus[]
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const RelaySettings: React.FC<RelaySettingsProps> = ({ relayStatuses, onClose }) => {
|
||||
const navigate = useNavigate()
|
||||
const activeRelays = relayStatuses.filter(r => r.isInPool)
|
||||
const recentRelays = relayStatuses.filter(r => !r.isInPool)
|
||||
|
||||
const handleLinkClick = (url: string) => {
|
||||
if (onClose) onClose()
|
||||
navigate(`/r/${encodeURIComponent(url)}`)
|
||||
}
|
||||
|
||||
const formatRelayUrl = (url: string) => {
|
||||
return url.replace(/^wss?:\/\//, '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
const formatLastSeen = (timestamp: number) => {
|
||||
try {
|
||||
return formatDistanceToNow(timestamp, { addSuffix: true })
|
||||
} catch {
|
||||
return 'just now'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h3>Relays</h3>
|
||||
|
||||
{activeRelays.length > 0 && (
|
||||
<div className="relay-group" style={{ marginBottom: '1.5rem' }}>
|
||||
<div className="relay-list">
|
||||
{activeRelays.map((relay) => (
|
||||
<div
|
||||
key={relay.url}
|
||||
className="relay-item"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
background: 'var(--surface-secondary)',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '0.5rem'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
style={{
|
||||
color: 'var(--success, #22c55e)',
|
||||
fontSize: '1rem'
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{formatRelayUrl(relay.url)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recentRelays.length > 0 && (
|
||||
<div className="relay-group">
|
||||
<h4 style={{
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '0.75rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
Recently Seen
|
||||
</h4>
|
||||
<div className="relay-list">
|
||||
{recentRelays.map((relay) => (
|
||||
<div
|
||||
key={relay.url}
|
||||
className="relay-item"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
background: 'var(--surface-secondary)',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '0.5rem',
|
||||
opacity: 0.7
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCircle}
|
||||
style={{
|
||||
color: 'var(--text-tertiary, #6b7280)',
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'var(--font-mono, monospace)',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{formatRelayUrl(relay.url)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
fontSize: '0.8rem',
|
||||
color: 'var(--text-tertiary)',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
{formatLastSeen(relay.lastSeen)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{relayStatuses.length === 0 && (
|
||||
<p style={{ color: 'var(--text-secondary)', fontStyle: 'italic' }}>
|
||||
No relay connections found
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
marginTop: '1.5rem',
|
||||
padding: '1rem',
|
||||
background: 'var(--surface-secondary)',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.9rem',
|
||||
lineHeight: '1.6'
|
||||
}}>
|
||||
<p style={{ margin: 0, color: 'var(--text-secondary)' }}>
|
||||
Boris works best with a local relay. Consider running{' '}
|
||||
<a
|
||||
href="https://github.com/greenart7c3/Citrine?tab=readme-ov-file#download"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'var(--accent, #8b5cf6)' }}
|
||||
>
|
||||
Citrine
|
||||
</a>
|
||||
{' or '}
|
||||
<a
|
||||
href="https://github.com/CodyTseng/nostr-relay-tray/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'var(--accent, #8b5cf6)' }}
|
||||
>
|
||||
nostr-relay-tray
|
||||
</a>
|
||||
. Don't know what relays are? Learn more{' '}
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleLinkClick('https://nostr.how/en/relays')
|
||||
}}
|
||||
style={{ color: 'var(--accent, #8b5cf6)', cursor: 'pointer' }}
|
||||
>
|
||||
here
|
||||
</a>
|
||||
{' and '}
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleLinkClick('https://davidebtc186.substack.com/p/the-importance-of-hosting-your-own')
|
||||
}}
|
||||
style={{ color: 'var(--accent, #8b5cf6)', cursor: 'pointer' }}
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RelaySettings
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react'
|
||||
import { faNetworkWired, faUserGroup, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { UserSettings } from '../../services/settingsService'
|
||||
import IconButton from '../IconButton'
|
||||
|
||||
interface StartupPreferencesSettingsProps {
|
||||
settings: UserSettings
|
||||
@@ -38,33 +36,6 @@ const StartupPreferencesSettings: React.FC<StartupPreferencesSettingsProps> = ({
|
||||
<span>Start with highlights panel collapsed</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Default Highlight Visibility</label>
|
||||
<div className="setting-buttons">
|
||||
<IconButton
|
||||
icon={faNetworkWired}
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityNostrverse: !(settings.defaultHighlightVisibilityNostrverse !== false) })}
|
||||
title="Nostrverse highlights"
|
||||
ariaLabel="Toggle nostrverse highlights by default"
|
||||
variant={(settings.defaultHighlightVisibilityNostrverse !== false) ? 'primary' : 'ghost'}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUserGroup}
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityFriends: !(settings.defaultHighlightVisibilityFriends !== false) })}
|
||||
title="Friends highlights"
|
||||
ariaLabel="Toggle friends highlights by default"
|
||||
variant={(settings.defaultHighlightVisibilityFriends !== false) ? 'primary' : 'ghost'}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUser}
|
||||
onClick={() => onUpdate({ defaultHighlightVisibilityMine: !(settings.defaultHighlightVisibilityMine !== false) })}
|
||||
title="My highlights"
|
||||
ariaLabel="Toggle my highlights by default"
|
||||
variant={(settings.defaultHighlightVisibilityMine !== false) ? 'primary' : 'ghost'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,19 @@ const ZapSettings: React.FC<ZapSettingsProps> = ({ settings, onUpdate }) => {
|
||||
const borisPercentage = totalWeight > 0 ? (borisWeight / totalWeight) * 100 : 0
|
||||
const authorPercentage = totalWeight > 0 ? (authorWeight / totalWeight) * 100 : 0
|
||||
|
||||
const presets = {
|
||||
default: { highlighter: 50, boris: 2.1, author: 50 },
|
||||
generous: { highlighter: 5, boris: 10, author: 75 },
|
||||
selfless: { highlighter: 1, boris: 19, author: 80 },
|
||||
boris: { highlighter: 10, boris: 80, author: 10 },
|
||||
}
|
||||
|
||||
const isPresetActive = (preset: { highlighter: number; boris: number; author: number }) => {
|
||||
return highlighterWeight === preset.highlighter &&
|
||||
borisWeight === preset.boris &&
|
||||
authorWeight === preset.author
|
||||
}
|
||||
|
||||
const applyPreset = (preset: { highlighter: number; boris: number; author: number }) => {
|
||||
onUpdate({
|
||||
zapSplitHighlighterWeight: preset.highlighter,
|
||||
@@ -33,29 +46,29 @@ const ZapSettings: React.FC<ZapSettingsProps> = ({ settings, onUpdate }) => {
|
||||
<label className="setting-label">Presets</label>
|
||||
<div className="zap-preset-buttons">
|
||||
<button
|
||||
onClick={() => applyPreset({ highlighter: 50, boris: 2.1, author: 50 })}
|
||||
className="zap-preset-btn"
|
||||
onClick={() => applyPreset(presets.default)}
|
||||
className={`zap-preset-btn ${isPresetActive(presets.default) ? 'active' : ''}`}
|
||||
title="You: 49%, Author: 49%, Boris: 2%"
|
||||
>
|
||||
Default
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyPreset({ highlighter: 5, boris: 10, author: 75 })}
|
||||
className="zap-preset-btn"
|
||||
onClick={() => applyPreset(presets.generous)}
|
||||
className={`zap-preset-btn ${isPresetActive(presets.generous) ? 'active' : ''}`}
|
||||
title="You: 6%, Author: 83%, Boris: 11%"
|
||||
>
|
||||
Generous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyPreset({ highlighter: 1, boris: 19, author: 80 })}
|
||||
className="zap-preset-btn"
|
||||
onClick={() => applyPreset(presets.selfless)}
|
||||
className={`zap-preset-btn ${isPresetActive(presets.selfless) ? 'active' : ''}`}
|
||||
title="You: 1%, Author: 80%, Boris: 19%"
|
||||
>
|
||||
Selfless
|
||||
</button>
|
||||
<button
|
||||
onClick={() => applyPreset({ highlighter: 10, boris: 80, author: 10 })}
|
||||
className="zap-preset-btn"
|
||||
onClick={() => applyPreset(presets.boris)}
|
||||
className={`zap-preset-btn ${isPresetActive(presets.boris) ? 'active' : ''}`}
|
||||
title="You: 10%, Author: 10%, Boris: 80%"
|
||||
>
|
||||
Boris 🧡
|
||||
|
||||
@@ -108,15 +108,6 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
|
||||
ariaLabel="Settings"
|
||||
variant="ghost"
|
||||
/>
|
||||
{activeAccount && (
|
||||
<IconButton
|
||||
icon={faPlus}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
title="Add bookmark"
|
||||
ariaLabel="Add bookmark"
|
||||
variant="ghost"
|
||||
/>
|
||||
)}
|
||||
{onRefresh && (
|
||||
<IconButton
|
||||
icon={faRotate}
|
||||
@@ -128,6 +119,15 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
|
||||
spin={isRefreshing}
|
||||
/>
|
||||
)}
|
||||
{activeAccount && (
|
||||
<IconButton
|
||||
icon={faPlus}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
title="Add bookmark"
|
||||
ariaLabel="Add bookmark"
|
||||
variant="ghost"
|
||||
/>
|
||||
)}
|
||||
{activeAccount ? (
|
||||
<IconButton
|
||||
icon={faRightFromBracket}
|
||||
|
||||
@@ -97,6 +97,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
settings={props.settings}
|
||||
onSave={props.onSaveSettings}
|
||||
onClose={props.onCloseSettings}
|
||||
relayPool={props.relayPool}
|
||||
/>
|
||||
) : (
|
||||
<ContentPanel
|
||||
@@ -105,6 +106,8 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||
html={props.readerContent?.html}
|
||||
markdown={props.readerContent?.markdown}
|
||||
image={props.readerContent?.image}
|
||||
summary={props.readerContent?.summary}
|
||||
published={props.readerContent?.published}
|
||||
selectedUrl={props.selectedUrl}
|
||||
highlights={props.classifiedHighlights}
|
||||
showHighlights={props.showHighlights}
|
||||
|
||||
@@ -49,6 +49,8 @@ export function useArticleLoader({
|
||||
title: article.title,
|
||||
markdown: article.markdown,
|
||||
image: article.image,
|
||||
summary: article.summary,
|
||||
published: article.published,
|
||||
url: `nostr:${naddr}`
|
||||
})
|
||||
|
||||
@@ -71,19 +73,22 @@ export function useArticleLoader({
|
||||
try {
|
||||
setHighlightsLoading(true)
|
||||
setHighlights([]) // Clear old highlights
|
||||
const highlightsList: Highlight[] = []
|
||||
const highlightsMap = new Map<string, Highlight>()
|
||||
|
||||
await fetchHighlightsForArticle(
|
||||
relayPool,
|
||||
articleCoordinate,
|
||||
article.event.id,
|
||||
(highlight) => {
|
||||
// Render each highlight immediately as it arrives
|
||||
highlightsList.push(highlight)
|
||||
setHighlights([...highlightsList].sort((a, b) => b.created_at - a.created_at))
|
||||
// Deduplicate highlights by ID as they arrive
|
||||
if (!highlightsMap.has(highlight.id)) {
|
||||
highlightsMap.set(highlight.id, highlight)
|
||||
const highlightsList = Array.from(highlightsMap.values())
|
||||
setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log(`📌 Found ${highlightsList.length} highlights`)
|
||||
console.log(`📌 Found ${highlightsMap.size} highlights`)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch highlights:', err)
|
||||
} finally {
|
||||
|
||||
@@ -55,17 +55,21 @@ export const useBookmarksData = ({
|
||||
setHighlightsLoading(true)
|
||||
try {
|
||||
if (currentArticleCoordinate) {
|
||||
const highlightsList: Highlight[] = []
|
||||
const highlightsMap = new Map<string, Highlight>()
|
||||
await fetchHighlightsForArticle(
|
||||
relayPool,
|
||||
currentArticleCoordinate,
|
||||
currentArticleEventId,
|
||||
(highlight) => {
|
||||
highlightsList.push(highlight)
|
||||
setHighlights([...highlightsList].sort((a, b) => b.created_at - a.created_at))
|
||||
// Deduplicate highlights by ID as they arrive
|
||||
if (!highlightsMap.has(highlight.id)) {
|
||||
highlightsMap.set(highlight.id, highlight)
|
||||
const highlightsList = Array.from(highlightsMap.values())
|
||||
setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log(`🔄 Refreshed ${highlightsList.length} highlights for article`)
|
||||
console.log(`🔄 Refreshed ${highlightsMap.size} highlights for article`)
|
||||
} else if (activeAccount) {
|
||||
const fetchedHighlights = await fetchHighlights(relayPool, activeAccount.pubkey)
|
||||
setHighlights(fetchedHighlights)
|
||||
|
||||
@@ -14,7 +14,6 @@ export const useBookmarksUI = ({ settings }: UseBookmarksUIParams) => {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('compact')
|
||||
const [showHighlights, setShowHighlights] = useState(true)
|
||||
const [selectedHighlightId, setSelectedHighlightId] = useState<string | undefined>(undefined)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [currentArticleCoordinate, setCurrentArticleCoordinate] = useState<string | undefined>(undefined)
|
||||
const [currentArticleEventId, setCurrentArticleEventId] = useState<string | undefined>(undefined)
|
||||
const [currentArticle, setCurrentArticle] = useState<NostrEvent | undefined>(undefined)
|
||||
@@ -46,8 +45,6 @@ export const useBookmarksUI = ({ settings }: UseBookmarksUIParams) => {
|
||||
setShowHighlights,
|
||||
selectedHighlightId,
|
||||
setSelectedHighlightId,
|
||||
showSettings,
|
||||
setShowSettings,
|
||||
currentArticleCoordinate,
|
||||
setCurrentArticleCoordinate,
|
||||
currentArticleEventId,
|
||||
|
||||
37
src/hooks/useRelayStatus.ts
Normal file
37
src/hooks/useRelayStatus.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { RelayStatus, updateAndGetRelayStatuses } from '../services/relayStatusService'
|
||||
|
||||
interface UseRelayStatusParams {
|
||||
relayPool: RelayPool | null
|
||||
pollingInterval?: number // in milliseconds
|
||||
}
|
||||
|
||||
export function useRelayStatus({
|
||||
relayPool,
|
||||
pollingInterval = 5000
|
||||
}: UseRelayStatusParams) {
|
||||
const [relayStatuses, setRelayStatuses] = useState<RelayStatus[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!relayPool) return
|
||||
|
||||
const updateStatuses = () => {
|
||||
const statuses = updateAndGetRelayStatuses(relayPool)
|
||||
setRelayStatuses(statuses)
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updateStatuses()
|
||||
|
||||
// Poll for updates
|
||||
const interval = setInterval(updateStatuses, pollingInterval)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [relayPool, pollingInterval])
|
||||
|
||||
return relayStatuses
|
||||
}
|
||||
|
||||
@@ -501,17 +501,20 @@ body {
|
||||
}
|
||||
|
||||
.reader-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.reader-title {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-family: var(--reading-font);
|
||||
}
|
||||
|
||||
.reader-summary {
|
||||
color: #aaa;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 1rem 0;
|
||||
font-family: var(--reading-font);
|
||||
}
|
||||
|
||||
.reader-meta {
|
||||
@@ -1070,6 +1073,8 @@ body {
|
||||
margin: 0 0 2rem 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.reader-hero-image img {
|
||||
@@ -1080,6 +1085,50 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.reader-header-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 2rem 2rem 1.5rem;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.6) 60%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
|
||||
.reader-header-overlay .reader-title {
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.reader-header-overlay .reader-summary {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 1rem 0;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
font-family: var(--reading-font);
|
||||
}
|
||||
|
||||
.reader-header-overlay .reader-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.reader-header-overlay .reading-time,
|
||||
.reader-header-overlay .highlight-indicator {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reader-header-overlay .highlight-indicator {
|
||||
background: rgba(100, 108, 255, 0.25);
|
||||
border: 1px solid rgba(100, 108, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Private Bookmark Styles */
|
||||
.private-bookmark {
|
||||
background: #2a2a2a;
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface ReadableContent {
|
||||
html?: string
|
||||
markdown?: string
|
||||
image?: string
|
||||
summary?: string
|
||||
published?: number
|
||||
}
|
||||
|
||||
interface CachedContent {
|
||||
@@ -57,7 +59,7 @@ function saveToCache(url: string, content: ReadableContent): void {
|
||||
function toProxyUrl(url: string): string {
|
||||
// Ensure the target URL has a protocol and build the proxy URL
|
||||
const normalized = /^https?:\/\//i.test(url) ? url : `https://${url}`
|
||||
return `https://r.jina.ai/http://${normalized.replace(/^https?:\/\//, '')}`
|
||||
return `https://r.jina.ai/${normalized}`
|
||||
}
|
||||
|
||||
export async function fetchReadableContent(
|
||||
|
||||
65
src/services/relayStatusService.ts
Normal file
65
src/services/relayStatusService.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
|
||||
export interface RelayStatus {
|
||||
url: string
|
||||
isInPool: boolean
|
||||
lastSeen: number // timestamp
|
||||
}
|
||||
|
||||
const RECENT_CONNECTION_WINDOW = 20 * 60 * 1000 // 20 minutes
|
||||
|
||||
// In-memory tracking of relay last seen times
|
||||
const relayLastSeen = new Map<string, number>()
|
||||
|
||||
/**
|
||||
* Updates and gets the current status of all relays
|
||||
*/
|
||||
export function updateAndGetRelayStatuses(relayPool: RelayPool): RelayStatus[] {
|
||||
const statuses: RelayStatus[] = []
|
||||
const now = Date.now()
|
||||
const currentRelayUrls = new Set<string>()
|
||||
|
||||
// Update relays currently in the pool
|
||||
for (const relay of relayPool.relays.values()) {
|
||||
currentRelayUrls.add(relay.url)
|
||||
relayLastSeen.set(relay.url, now)
|
||||
|
||||
statuses.push({
|
||||
url: relay.url,
|
||||
isInPool: true,
|
||||
lastSeen: now
|
||||
})
|
||||
}
|
||||
|
||||
// Add recently seen relays that are no longer in the pool
|
||||
const cutoffTime = now - RECENT_CONNECTION_WINDOW
|
||||
for (const [url, lastSeen] of relayLastSeen.entries()) {
|
||||
if (!currentRelayUrls.has(url) && lastSeen >= cutoffTime) {
|
||||
statuses.push({
|
||||
url,
|
||||
isInPool: false,
|
||||
lastSeen
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old entries
|
||||
for (const [url, lastSeen] of relayLastSeen.entries()) {
|
||||
if (lastSeen < cutoffTime) {
|
||||
relayLastSeen.delete(url)
|
||||
}
|
||||
}
|
||||
|
||||
return statuses.sort((a, b) => {
|
||||
if (a.isInPool !== b.isInPool) return a.isInPool ? -1 : 1
|
||||
return b.lastSeen - a.lastSeen
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets count of currently active relays
|
||||
*/
|
||||
export function getActiveCount(statuses: RelayStatus[]): number {
|
||||
return statuses.filter(r => r.isInPool).length
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function loadContent(
|
||||
title: article.title,
|
||||
markdown: article.markdown,
|
||||
image: article.image,
|
||||
summary: article.summary,
|
||||
url: `nostr:${naddr}`
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user