mirror of
https://github.com/dergigi/boris.git
synced 2025-12-20 08:04:30 +01:00
feat: automatic offline sync - rebroadcast local events when back online
- Create offlineSyncService to sync local-only events to remote relays - Create useOfflineSync hook to detect online/offline transitions - When user comes back online (remote relays connect), automatically: - Query local relays for user's events from last 24 hours - Rebroadcast highlights and bookmarks to remote relays - Integrate sync into Bookmarks component - Enables seamless offline workflow: - User can work offline with local relays - Events are automatically synced when connection restored - No manual intervention required
This commit is contained in:
@@ -10,6 +10,8 @@ import { useBookmarksData } from '../hooks/useBookmarksData'
|
|||||||
import { useContentSelection } from '../hooks/useContentSelection'
|
import { useContentSelection } from '../hooks/useContentSelection'
|
||||||
import { useHighlightCreation } from '../hooks/useHighlightCreation'
|
import { useHighlightCreation } from '../hooks/useHighlightCreation'
|
||||||
import { useBookmarksUI } from '../hooks/useBookmarksUI'
|
import { useBookmarksUI } from '../hooks/useBookmarksUI'
|
||||||
|
import { useRelayStatus } from '../hooks/useRelayStatus'
|
||||||
|
import { useOfflineSync } from '../hooks/useOfflineSync'
|
||||||
import ThreePaneLayout from './ThreePaneLayout'
|
import ThreePaneLayout from './ThreePaneLayout'
|
||||||
import { classifyHighlights } from '../utils/highlightClassification'
|
import { classifyHighlights } from '../utils/highlightClassification'
|
||||||
|
|
||||||
@@ -50,6 +52,17 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
accountManager
|
accountManager
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Monitor relay status for offline sync
|
||||||
|
const relayStatuses = useRelayStatus({ relayPool, pollingInterval: 5000 })
|
||||||
|
|
||||||
|
// Automatically sync local events to remote relays when coming back online
|
||||||
|
useOfflineSync({
|
||||||
|
relayPool,
|
||||||
|
account: activeAccount,
|
||||||
|
relayStatuses,
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
setIsCollapsed,
|
setIsCollapsed,
|
||||||
|
|||||||
57
src/hooks/useOfflineSync.ts
Normal file
57
src/hooks/useOfflineSync.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { RelayPool } from 'applesauce-relay'
|
||||||
|
import { IAccount } from 'applesauce-core/helpers'
|
||||||
|
import { syncLocalEventsToRemote } from '../services/offlineSyncService'
|
||||||
|
import { isLocalRelay } from '../utils/helpers'
|
||||||
|
import { RelayStatus } from '../services/relayStatusService'
|
||||||
|
|
||||||
|
interface UseOfflineSyncParams {
|
||||||
|
relayPool: RelayPool | null
|
||||||
|
account: IAccount | null
|
||||||
|
relayStatuses: RelayStatus[]
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOfflineSync({
|
||||||
|
relayPool,
|
||||||
|
account,
|
||||||
|
relayStatuses,
|
||||||
|
enabled = true
|
||||||
|
}: UseOfflineSyncParams) {
|
||||||
|
const previousStateRef = useRef<{
|
||||||
|
hasRemoteRelays: boolean
|
||||||
|
initialized: boolean
|
||||||
|
}>({
|
||||||
|
hasRemoteRelays: false,
|
||||||
|
initialized: false
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !relayPool || !account) return
|
||||||
|
|
||||||
|
const connectedRelays = relayStatuses.filter(r => r.isInPool)
|
||||||
|
const hasRemoteRelays = connectedRelays.some(r => !isLocalRelay(r.url))
|
||||||
|
const hasLocalRelays = connectedRelays.some(r => isLocalRelay(r.url))
|
||||||
|
|
||||||
|
// Skip the first check to avoid syncing on initial load
|
||||||
|
if (!previousStateRef.current.initialized) {
|
||||||
|
previousStateRef.current = {
|
||||||
|
hasRemoteRelays,
|
||||||
|
initialized: true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect transition: from local-only to having remote relays
|
||||||
|
const wasLocalOnly = !previousStateRef.current.hasRemoteRelays && hasLocalRelays
|
||||||
|
const isNowOnline = hasRemoteRelays
|
||||||
|
|
||||||
|
if (wasLocalOnly && isNowOnline) {
|
||||||
|
console.log('✈️ Detected transition: Flight Mode → Online')
|
||||||
|
syncLocalEventsToRemote(relayPool, account, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousStateRef.current.hasRemoteRelays = hasRemoteRelays
|
||||||
|
}, [relayPool, account, relayStatuses, enabled])
|
||||||
|
}
|
||||||
|
|
||||||
126
src/services/offlineSyncService.ts
Normal file
126
src/services/offlineSyncService.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { RelayPool } from 'applesauce-relay'
|
||||||
|
import { NostrEvent } from 'nostr-tools'
|
||||||
|
import { IAccount } from 'applesauce-core/helpers'
|
||||||
|
import { RELAYS } from '../config/relays'
|
||||||
|
import { isLocalRelay } from '../utils/helpers'
|
||||||
|
|
||||||
|
interface SyncState {
|
||||||
|
lastRemoteConnectionTime: number
|
||||||
|
wasLocalOnly: boolean
|
||||||
|
isSyncing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncState: SyncState = {
|
||||||
|
lastRemoteConnectionTime: 0,
|
||||||
|
wasLocalOnly: false,
|
||||||
|
isSyncing: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs local-only events to remote relays when coming back online
|
||||||
|
*/
|
||||||
|
export async function syncLocalEventsToRemote(
|
||||||
|
relayPool: RelayPool,
|
||||||
|
account: IAccount | null,
|
||||||
|
hasRemoteRelays: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
if (!account || syncState.isSyncing) return
|
||||||
|
|
||||||
|
// Detect transition from local-only to having remote relays
|
||||||
|
const justCameOnline = !syncState.wasLocalOnly && hasRemoteRelays
|
||||||
|
syncState.wasLocalOnly = !hasRemoteRelays
|
||||||
|
|
||||||
|
if (!justCameOnline) return
|
||||||
|
|
||||||
|
console.log('🔄 Coming back online - syncing local events to remote relays...')
|
||||||
|
syncState.isSyncing = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localRelays = RELAYS.filter(isLocalRelay)
|
||||||
|
const remoteRelays = RELAYS.filter(url => !isLocalRelay(url))
|
||||||
|
|
||||||
|
if (localRelays.length === 0 || remoteRelays.length === 0) {
|
||||||
|
console.log('⚠️ No local or remote relays available for sync')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get events from local relays that were created in the last 24 hours
|
||||||
|
const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60)
|
||||||
|
const eventsToSync: NostrEvent[] = []
|
||||||
|
|
||||||
|
// Query for user's events from local relays
|
||||||
|
const filters = [
|
||||||
|
{ kinds: [9802], authors: [account.pubkey], since }, // Highlights
|
||||||
|
{ kinds: [10003, 30003], authors: [account.pubkey], since }, // Bookmarks
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
const events = await new Promise<NostrEvent[]>((resolve) => {
|
||||||
|
const collected: NostrEvent[] = []
|
||||||
|
const sub = relayPool.req(localRelays, filter, {
|
||||||
|
onevent: (event: NostrEvent) => {
|
||||||
|
collected.push(event)
|
||||||
|
},
|
||||||
|
oneose: () => {
|
||||||
|
sub.close()
|
||||||
|
resolve(collected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
sub.close()
|
||||||
|
resolve(collected)
|
||||||
|
}, 5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
eventsToSync.push(...events)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventsToSync.length === 0) {
|
||||||
|
console.log('✅ No local events to sync')
|
||||||
|
syncState.isSyncing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate events by id
|
||||||
|
const uniqueEvents = Array.from(
|
||||||
|
new Map(eventsToSync.map(e => [e.id, e])).values()
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`📤 Syncing ${uniqueEvents.length} event(s) to remote relays...`)
|
||||||
|
|
||||||
|
// Publish to remote relays
|
||||||
|
let successCount = 0
|
||||||
|
for (const event of uniqueEvents) {
|
||||||
|
try {
|
||||||
|
await relayPool.publish(remoteRelays, event)
|
||||||
|
successCount++
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`⚠️ Failed to sync event ${event.id.slice(0, 8)}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Synced ${successCount}/${uniqueEvents.length} events to remote relays`)
|
||||||
|
syncState.lastRemoteConnectionTime = Date.now()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during offline sync:', error)
|
||||||
|
} finally {
|
||||||
|
syncState.isSyncing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if we should sync based on current relay state
|
||||||
|
*/
|
||||||
|
export function shouldSync(
|
||||||
|
hasRemoteRelays: boolean,
|
||||||
|
account: IAccount | null
|
||||||
|
): boolean {
|
||||||
|
if (!account || syncState.isSyncing) return false
|
||||||
|
|
||||||
|
// Only sync if we just came online (transition from local-only to having remote relays)
|
||||||
|
const justCameOnline = syncState.wasLocalOnly && hasRemoteRelays
|
||||||
|
return justCameOnline
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user