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:
Gigi
2025-10-09 13:56:12 +01:00
parent d294287c64
commit a2041bd14d
4 changed files with 94 additions and 5 deletions

View File

@@ -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">

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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
}