mirror of
https://github.com/dergigi/boris.git
synced 2025-12-22 00:54:21 +01:00
feat(settings): add Relays section showing active and recently connected relays
- Add relayStatusService to track relay connections with 20-minute history - Add useRelayStatus hook for polling relay status updates - Create RelaySettings component to display active and recent relays - Update Settings and ThreePaneLayout to integrate relay status display - Shows relay connection status with visual indicators and timestamps
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { faTimes, faUndo } from '@fortawesome/free-solid-svg-icons'
|
import { faTimes, faUndo } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { UserSettings } from '../services/settingsService'
|
import { UserSettings } from '../services/settingsService'
|
||||||
import IconButton from './IconButton'
|
import IconButton from './IconButton'
|
||||||
import { loadFont } from '../utils/fontLoader'
|
import { loadFont } from '../utils/fontLoader'
|
||||||
@@ -7,6 +8,8 @@ import ReadingDisplaySettings from './Settings/ReadingDisplaySettings'
|
|||||||
import LayoutNavigationSettings from './Settings/LayoutNavigationSettings'
|
import LayoutNavigationSettings from './Settings/LayoutNavigationSettings'
|
||||||
import StartupPreferencesSettings from './Settings/StartupPreferencesSettings'
|
import StartupPreferencesSettings from './Settings/StartupPreferencesSettings'
|
||||||
import ZapSettings from './Settings/ZapSettings'
|
import ZapSettings from './Settings/ZapSettings'
|
||||||
|
import RelaySettings from './Settings/RelaySettings'
|
||||||
|
import { useRelayStatus } from '../hooks/useRelayStatus'
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: UserSettings = {
|
const DEFAULT_SETTINGS: UserSettings = {
|
||||||
collapseOnArticleOpen: true,
|
collapseOnArticleOpen: true,
|
||||||
@@ -33,9 +36,10 @@ interface SettingsProps {
|
|||||||
settings: UserSettings
|
settings: UserSettings
|
||||||
onSave: (settings: UserSettings) => Promise<void>
|
onSave: (settings: UserSettings) => Promise<void>
|
||||||
onClose: () => 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>(() => {
|
const [localSettings, setLocalSettings] = useState<UserSettings>(() => {
|
||||||
// Migrate old settings format to new weight-based format
|
// Migrate old settings format to new weight-based format
|
||||||
const migrated = { ...settings }
|
const migrated = { ...settings }
|
||||||
@@ -53,6 +57,8 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
|||||||
const saveTimeoutRef = useRef<number | null>(null)
|
const saveTimeoutRef = useRef<number | null>(null)
|
||||||
const isLocallyUpdating = useRef(false)
|
const isLocallyUpdating = useRef(false)
|
||||||
|
|
||||||
|
const relayStatuses = useRelayStatus({ relayPool })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't update from external settings if we're currently making local changes
|
// Don't update from external settings if we're currently making local changes
|
||||||
if (isLocallyUpdating.current) {
|
if (isLocallyUpdating.current) {
|
||||||
@@ -152,6 +158,7 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
|||||||
<LayoutNavigationSettings settings={localSettings} onUpdate={handleUpdate} />
|
<LayoutNavigationSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||||
<StartupPreferencesSettings settings={localSettings} onUpdate={handleUpdate} />
|
<StartupPreferencesSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||||
<ZapSettings settings={localSettings} onUpdate={handleUpdate} />
|
<ZapSettings settings={localSettings} onUpdate={handleUpdate} />
|
||||||
|
<RelaySettings relayStatuses={relayStatuses} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
164
src/components/Settings/RelaySettings.tsx
Normal file
164
src/components/Settings/RelaySettings.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React from 'react'
|
||||||
|
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[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelaySettings: React.FC<RelaySettingsProps> = ({ relayStatuses }) => {
|
||||||
|
const activeRelays = relayStatuses.filter(r => r.isInPool)
|
||||||
|
const recentRelays = relayStatuses.filter(r => !r.isInPool)
|
||||||
|
|
||||||
|
const formatRelayUrl = (url: string) => {
|
||||||
|
return url.replace(/^wss?:\/\//, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatLastSeen = (timestamp: number) => {
|
||||||
|
try {
|
||||||
|
return formatDistanceToNow(timestamp, { addSuffix: true })
|
||||||
|
} catch {
|
||||||
|
return 'just now'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-section">
|
||||||
|
<h3>Relays</h3>
|
||||||
|
|
||||||
|
<div className="relay-summary" style={{ marginBottom: '1rem' }}>
|
||||||
|
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
|
||||||
|
{activeRelays.length} active relay{activeRelays.length !== 1 ? 's' : ''}
|
||||||
|
{recentRelays.length > 0 &&
|
||||||
|
` · ${recentRelays.length} recently seen`
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeRelays.length > 0 && (
|
||||||
|
<div className="relay-group" style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<h4 style={{
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.05em'
|
||||||
|
}}>
|
||||||
|
Active
|
||||||
|
</h4>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RelaySettings
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
settings={props.settings}
|
settings={props.settings}
|
||||||
onSave={props.onSaveSettings}
|
onSave={props.onSaveSettings}
|
||||||
onClose={props.onCloseSettings}
|
onClose={props.onCloseSettings}
|
||||||
|
relayPool={props.relayPool}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ContentPanel
|
<ContentPanel
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user