feat: add relay status indicator for local-only/offline mode

- Create RelayStatusIndicator component with plane icon for local-only mode
- Position indicator in bottom-left corner with amber styling
- Show when only local relays are connected or completely offline
- Hide indicator when remote relays are available (normal operation)
- Add pulsing globe icon animation to indicate checking for connection
- Include hover effects and smooth transitions
- Auto-adjust position when sidebar is collapsed
- Display relay count and clear status messages
This commit is contained in:
Gigi
2025-10-09 12:47:29 +01:00
parent bdab9c06e4
commit 4cf2ac9172
3 changed files with 143 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlane, faGlobe, faCircle } from '@fortawesome/free-solid-svg-icons'
import { RelayPool } from 'applesauce-relay'
import { useRelayStatus } from '../hooks/useRelayStatus'
import { isLocalRelay } from '../utils/helpers'
interface RelayStatusIndicatorProps {
relayPool: RelayPool | null
}
export const RelayStatusIndicator: React.FC<RelayStatusIndicatorProps> = ({ relayPool }) => {
const relayStatuses = useRelayStatus({ relayPool, pollingInterval: 3000 })
if (!relayPool) return null
// Get currently connected relays
const connectedRelays = relayStatuses.filter(r => r.isInPool)
const connectedUrls = connectedRelays.map(r => r.url)
// Determine connection status
const hasLocalRelay = connectedUrls.some(url => isLocalRelay(url))
const hasRemoteRelay = connectedUrls.some(url => !isLocalRelay(url))
const localOnlyMode = hasLocalRelay && !hasRemoteRelay
const offlineMode = connectedUrls.length === 0
// Don't show indicator when fully connected
if (!localOnlyMode && !offlineMode) return null
return (
<div className="relay-status-indicator" title={
offlineMode
? 'Offline - No relays connected'
: 'Local Relays Only - Highlights will be marked as local'
}>
<div className="relay-status-icon">
<FontAwesomeIcon icon={offlineMode ? faCircle : faPlane} />
</div>
<div className="relay-status-text">
{offlineMode ? (
<>
<span className="relay-status-title">Offline</span>
<span className="relay-status-subtitle">No relays connected</span>
</>
) : (
<>
<span className="relay-status-title">Local Only</span>
<span className="relay-status-subtitle">{connectedUrls.length} local relay{connectedUrls.length !== 1 ? 's' : ''}</span>
</>
)}
</div>
{!offlineMode && (
<div className="relay-status-pulse">
<FontAwesomeIcon icon={faGlobe} className="pulse-icon" />
</div>
)}
</div>
)
}

View File

@@ -6,6 +6,7 @@ import { HighlightsPanel } from './HighlightsPanel'
import Settings from './Settings'
import Toast from './Toast'
import { HighlightButton } from './HighlightButton'
import { RelayStatusIndicator } from './RelayStatusIndicator'
import { ViewMode } from './Bookmarks'
import { Bookmark } from '../types/bookmarks'
import { Highlight } from '../types/highlights'
@@ -149,6 +150,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
highlightColor={props.settings.highlightColor || '#ffff00'}
/>
)}
<RelayStatusIndicator relayPool={props.relayPool} />
{props.toastMessage && (
<Toast
message={props.toastMessage}

View File

@@ -2426,3 +2426,84 @@ body {
opacity: 0.6;
cursor: not-allowed;
}
/* Relay Status Indicator */
.relay-status-indicator {
position: fixed;
bottom: 1.5rem;
left: 1.5rem;
z-index: 999;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(245, 158, 11, 0.95);
border: 1px solid rgba(245, 158, 11, 0.4);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
cursor: default;
}
.relay-status-indicator:hover {
background: rgba(245, 158, 11, 1);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
.relay-status-icon {
font-size: 1.25rem;
color: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
}
.relay-status-text {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.relay-status-title {
font-size: 0.875rem;
font-weight: 600;
color: #1a1a1a;
line-height: 1.2;
}
.relay-status-subtitle {
font-size: 0.75rem;
color: rgba(26, 26, 26, 0.8);
line-height: 1.2;
}
.relay-status-pulse {
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.25rem;
}
.pulse-icon {
font-size: 0.875rem;
color: rgba(26, 26, 26, 0.6);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 0.4;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.1);
}
}
/* Adjust for collapsed sidebar */
.three-pane.sidebar-collapsed .relay-status-indicator {
left: calc(var(--sidebar-collapsed-width) + 1.5rem);
}