diff --git a/src/components/HighlightItem.tsx b/src/components/HighlightItem.tsx index 04f2bc06..093efb47 100644 --- a/src/components/HighlightItem.tsx +++ b/src/components/HighlightItem.tsx @@ -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 = ({ highlight, onSelectUrl, isSelected, onHighlightClick }) => { const itemRef = useRef(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 = ({ 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 = ({ highlight, onSelec )} - {highlight.isOfflineCreated && ( + {isSyncing && ( + <> + + + + + + )} + + {!isSyncing && showOfflineIndicator && ( <> diff --git a/src/index.css b/src/index.css index 6cd79096..f629cafa 100644 --- a/src/index.css +++ b/src/index.css @@ -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; diff --git a/src/services/offlineSyncService.ts b/src/services/offlineSyncService.ts index de563ffb..36d5db4b 100644 --- a/src/services/offlineSyncService.ts +++ b/src/services/offlineSyncService.ts @@ -9,6 +9,12 @@ let isSyncing = false // Track events created during offline period const offlineCreatedEvents = new Set() +// Track events currently being synced +const syncingEvents = new Set() + +// 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 { diff --git a/src/types/highlights.ts b/src/types/highlights.ts index f13e3be1..2f576fb7 100644 --- a/src/types/highlights.ts +++ b/src/types/highlights.ts @@ -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 }