diff --git a/packages/ui/src/apps/MobileApp.tsx b/packages/ui/src/apps/MobileApp.tsx index 7e7c1ea9..825b523d 100644 --- a/packages/ui/src/apps/MobileApp.tsx +++ b/packages/ui/src/apps/MobileApp.tsx @@ -202,19 +202,30 @@ const useNativeMobileChrome = (): void => { }; const useNativeMobileLifecycle = (onResume: () => void): void => { + const wasInactiveRef = React.useRef(false); + React.useEffect(() => { if (!isCapacitorMobileApp()) return; let disposed = false; const cleanup: Array<() => void> = []; + const resumeAfterInactive = () => { + if (!wasInactiveRef.current) return; + wasInactiveRef.current = false; + onResume(); + }; void import('@capacitor/app').then(async ({ App }) => { if (disposed) return; const state = await App.addListener('appStateChange', ({ isActive }) => { document.documentElement.classList.toggle('oc-native-app-active', isActive); - if (isActive) onResume(); + if (!isActive) { + wasInactiveRef.current = true; + return; + } + resumeAfterInactive(); }); - const resume = await App.addListener('resume', onResume); + const resume = await App.addListener('resume', resumeAfterInactive); if (disposed) { void state.remove(); void resume.remove(); diff --git a/packages/ui/src/sync/session-actions.ts b/packages/ui/src/sync/session-actions.ts index 8d4fe796..e0dc66c5 100644 --- a/packages/ui/src/sync/session-actions.ts +++ b/packages/ui/src/sync/session-actions.ts @@ -1169,6 +1169,10 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str // can't repopulate (and un-evict) a session already navigated away from. if (useSessionUIStore.getState().currentSessionId !== sessionID) return + const latestState = store.getState() + const latestStatus = getSessionMaterializationStatus(latestState, sessionID) + if (latestStatus.renderable && (latestState.message[sessionID]?.length ?? 0) >= records.length) return + store.setState((state) => { const materialized = materializeSessionSnapshots( state, @@ -1179,6 +1183,7 @@ export async function fetchMessagesForSession(sessionID: string, directory?: str })), { skipPartTypes: MESSAGE_REFETCH_SKIP_PARTS }, ) + if (!materialized.messagesChanged && !materialized.partsChanged) return state return { message: materialized.message, part: materialized.part } }) } catch { diff --git a/packages/ui/src/sync/sync-context.tsx b/packages/ui/src/sync/sync-context.tsx index 89ace18f..6d94b74b 100644 --- a/packages/ui/src/sync/sync-context.tsx +++ b/packages/ui/src/sync/sync-context.tsx @@ -277,15 +277,18 @@ async function materializeSessionFromServer( if (options?.isStale?.()) return store.setState((state: DirectoryStore) => { - const materialized = materializeSessionSnapshots( - state, - sessionID, + const materialized = materializeSessionSnapshots( + state, + sessionID, records.map((record: { info: Message; parts?: Part[] }) => ({ info: stripMessageDiffSnapshots(record.info), parts: record.parts ?? [], - })), + })), { skipPartTypes: RECONNECT_SKIP_PARTS }, ) + if (!materialized.messagesChanged && !materialized.partsChanged) { + return state + } return { message: materialized.message, part: materialized.part } }) @@ -1705,9 +1708,11 @@ export function SyncProvider(props: { const lastStatusPollAtByDirectoryRef = useRef(new Map()) const lastFullResyncAtByDirectoryRef = useRef(new Map()) const lastChildDiscoveryAtByDirectoryRef = useRef(new Map()) - const resyncingDirectoriesRef = useRef(new Set()) - const statusPollingDirectoriesRef = useRef(new Set()) - const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null) + const resyncingDirectoriesRef = useRef(new Set()) + const statusPollingDirectoriesRef = useRef(new Set()) + const pipelineReconnectRef = useRef<((reason?: string) => void) | null>(null) + const pipelineHasConnectedRef = useRef(false) + const pipelineDisconnectedBeforeFirstConnectRef = useRef(false) const system = useMemo( () => ({ @@ -1903,23 +1908,31 @@ export function SyncProvider(props: { } handleEvent(directory, payload, childStores, routingIndex) }, - onReconnect: () => { - useConfigStore.setState({ - isConnected: true, - hasEverConnected: true, - connectionPhase: "connected", - }) - if (isRecentBoot()) { - return - } + onReconnect: () => { + useConfigStore.setState({ + isConnected: true, + hasEverConnected: true, + connectionPhase: "connected", + }) + const isFirstConnect = !pipelineHasConnectedRef.current + pipelineHasConnectedRef.current = true + if (isFirstConnect && !pipelineDisconnectedBeforeFirstConnectRef.current) { + return + } + if (isRecentBoot()) { + return + } for (const dir of childStores.children.keys()) { triggerDirectoryResync(dir, "stream-reconnect") } - }, - onDisconnect: (reason) => { - const { hasEverConnected } = useConfigStore.getState() - useConfigStore.setState({ - isConnected: false, + }, + onDisconnect: (reason) => { + if (!pipelineHasConnectedRef.current) { + pipelineDisconnectedBeforeFirstConnectRef.current = true + } + const { hasEverConnected } = useConfigStore.getState() + useConfigStore.setState({ + isConnected: false, connectionPhase: hasEverConnected ? "reconnecting" : "connecting", lastDisconnectReason: reason, })