mirror of
https://github.com/dergigi/boris.git
synced 2025-12-19 07:34:28 +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 { 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 }
|
||||
@@ -53,6 +57,8 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
||||
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
|
||||
if (isLocallyUpdating.current) {
|
||||
@@ -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} />
|
||||
</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}
|
||||
onSave={props.onSaveSettings}
|
||||
onClose={props.onCloseSettings}
|
||||
relayPool={props.relayPool}
|
||||
/>
|
||||
) : (
|
||||
<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