mirror of
https://github.com/dergigi/boris.git
synced 2026-01-03 23:14:36 +01:00
feat: show sync progress and hide indicator after successful sync
- Show spinning blue icon while event is syncing to remote relays - Hide offline indicator completely after successful sync - Add sync state tracking with listeners for real-time updates - Track successful vs failed syncs separately - Only clear offline flag for successfully synced events - Blue spinner (#3b82f6) indicates active sync - Clean UI: no indicator after sync completes Behavior: 1. Create highlight offline → plane icon 2. Come back online → spinner replaces plane 3. Sync completes → no indicator (clean) 4. Sync fails → plane icon returns
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faQuoteLeft, faExternalLinkAlt, faHouseSignal, faPlane } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faQuoteLeft, faExternalLinkAlt, faHouseSignal, faPlane, faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Highlight } from '../types/highlights'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { onSyncStateChange, isEventSyncing } from '../services/offlineSyncService'
|
||||
|
||||
interface HighlightWithLevel extends Highlight {
|
||||
level?: 'mine' | 'friends' | 'nostrverse'
|
||||
@@ -19,6 +20,8 @@ interface HighlightItemProps {
|
||||
|
||||
export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelectUrl, isSelected, onHighlightClick }) => {
|
||||
const itemRef = useRef<HTMLDivElement>(null)
|
||||
const [isSyncing, setIsSyncing] = useState(() => isEventSyncing(highlight.id))
|
||||
const [showOfflineIndicator, setShowOfflineIndicator] = useState(() => highlight.isOfflineCreated && !isSyncing)
|
||||
|
||||
// Resolve the profile of the user who made the highlight
|
||||
const profile = useEventModel(Models.ProfileModel, [highlight.pubkey])
|
||||
@@ -30,6 +33,21 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelec
|
||||
return `${highlight.pubkey.slice(0, 8)}...` // fallback to short pubkey
|
||||
}
|
||||
|
||||
// Listen to sync state changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = onSyncStateChange((eventId, syncingState) => {
|
||||
if (eventId === highlight.id) {
|
||||
setIsSyncing(syncingState)
|
||||
// Hide offline indicator when sync completes successfully
|
||||
if (!syncingState) {
|
||||
setShowOfflineIndicator(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [highlight.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && itemRef.current) {
|
||||
itemRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
@@ -101,7 +119,16 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({ highlight, onSelec
|
||||
</>
|
||||
)}
|
||||
|
||||
{highlight.isOfflineCreated && (
|
||||
{isSyncing && (
|
||||
<>
|
||||
<span className="highlight-meta-separator">•</span>
|
||||
<span className="highlight-syncing-indicator" title="Syncing to remote relays...">
|
||||
<FontAwesomeIcon icon={faSpinner} spin />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isSyncing && showOfflineIndicator && (
|
||||
<>
|
||||
<span className="highlight-meta-separator">•</span>
|
||||
<span className="highlight-offline-indicator" title="Created while in flight mode">
|
||||
|
||||
@@ -1631,6 +1631,14 @@ body {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.highlight-syncing-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #3b82f6;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.highlight-local-text {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -9,6 +9,12 @@ let isSyncing = false
|
||||
// Track events created during offline period
|
||||
const offlineCreatedEvents = new Set<string>()
|
||||
|
||||
// Track events currently being synced
|
||||
const syncingEvents = new Set<string>()
|
||||
|
||||
// Callbacks to notify when sync state changes
|
||||
const syncStateListeners: Array<(eventId: string, isSyncing: boolean) => void> = []
|
||||
|
||||
/**
|
||||
* Marks an event as created during offline period
|
||||
*/
|
||||
@@ -17,6 +23,31 @@ export function markEventAsOfflineCreated(eventId: string): void {
|
||||
console.log(`📝 Marked event ${eventId.slice(0, 8)} as offline-created. Total: ${offlineCreatedEvents.size}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event is currently being synced
|
||||
*/
|
||||
export function isEventSyncing(eventId: string): boolean {
|
||||
return syncingEvents.has(eventId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to sync state changes
|
||||
*/
|
||||
export function onSyncStateChange(callback: (eventId: string, isSyncing: boolean) => void): () => void {
|
||||
syncStateListeners.push(callback)
|
||||
return () => {
|
||||
const index = syncStateListeners.indexOf(callback)
|
||||
if (index > -1) syncStateListeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners of sync state change
|
||||
*/
|
||||
function notifySyncStateChange(eventId: string, isSyncing: boolean): void {
|
||||
syncStateListeners.forEach(listener => listener(eventId, isSyncing))
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs local-only events to remote relays when coming back online
|
||||
* Now uses applesauce EventStore instead of querying relays
|
||||
@@ -82,12 +113,22 @@ export async function syncLocalEventsToRemote(
|
||||
|
||||
console.log(`📤 Syncing ${uniqueEvents.length} event(s) to remote relays...`)
|
||||
|
||||
// Mark all events as syncing
|
||||
uniqueEvents.forEach(event => {
|
||||
syncingEvents.add(event.id)
|
||||
notifySyncStateChange(event.id, true)
|
||||
})
|
||||
|
||||
// Publish to remote relays
|
||||
let successCount = 0
|
||||
const successfulIds: string[] = []
|
||||
|
||||
for (const event of uniqueEvents) {
|
||||
try {
|
||||
await relayPool.publish(remoteRelays, event)
|
||||
successCount++
|
||||
successfulIds.push(event.id)
|
||||
console.log(`✅ Synced event ${event.id.slice(0, 8)}`)
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to sync event ${event.id.slice(0, 8)}:`, error)
|
||||
}
|
||||
@@ -95,8 +136,20 @@ export async function syncLocalEventsToRemote(
|
||||
|
||||
console.log(`✅ Synced ${successCount}/${uniqueEvents.length} events to remote relays`)
|
||||
|
||||
// Clear offline events tracking after successful sync
|
||||
offlineCreatedEvents.clear()
|
||||
// Clear syncing state and offline tracking for successful events
|
||||
successfulIds.forEach(eventId => {
|
||||
syncingEvents.delete(eventId)
|
||||
offlineCreatedEvents.delete(eventId)
|
||||
notifySyncStateChange(eventId, false)
|
||||
})
|
||||
|
||||
// Clear syncing state for failed events
|
||||
uniqueEvents.forEach(event => {
|
||||
if (!successfulIds.includes(event.id)) {
|
||||
syncingEvents.delete(event.id)
|
||||
notifySyncStateChange(event.id, false)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Error during offline sync:', error)
|
||||
} finally {
|
||||
|
||||
@@ -19,5 +19,6 @@ export interface Highlight {
|
||||
publishedRelays?: string[] // URLs of relays that acknowledged this event
|
||||
isLocalOnly?: boolean // true if only published to local relays
|
||||
isOfflineCreated?: boolean // true if created while in flight mode (offline)
|
||||
isSyncing?: boolean // true if currently being synced to remote relays
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user