Skip to content

fix(ios): propagation sync sheet — manual-only presentation + styling#95

Merged
torlando-tech merged 2 commits into
mainfrom
fix/sync-status-sheet-manual-only
Jun 20, 2026
Merged

fix(ios): propagation sync sheet — manual-only presentation + styling#95
torlando-tech merged 2 commits into
mainfrom
fix/sync-status-sheet-manual-only

Conversation

@torlando-tech

Copy link
Copy Markdown
Owner

Problem

Two issues with the propagation-node sync status sheet on the Chats screen:

  1. It popped up automatically on every sync. The sheet was auto-presented via .onChange(of: syncState.isSyncing), which fires for all sync sources — background, periodic, the on-foreground auto-sync (ColumbaApp.swift, runs whenever the app becomes active and >60s since last sync), and Model B's NE-driven periodic sync — not just user-initiated ones. So it interrupted constantly.
  2. It looked broken. .background(Theme.backgroundPrimary.ignoresSafeArea()) was applied to the content, painting an opaque band sized to the content inside the system's default glass sheet chrome — an opaque edge-to-edge block floating in a transparent sheet.

Changes

  • ChatsView.swift — Removed the auto-present .onChange. The sheet is now shown only when the refresh button is tapped (toolbar + empty-state). Background / periodic / on-foreground / pull-to-refresh syncs update syncState silently. Pull-to-refresh keeps its own native indicator.
  • SyncStatusBottomSheet.swift — Replaced the content .background(...) with .presentationBackground(Theme.backgroundPrimary) (the SwiftUI-correct way, iOS 16.4+, to color the whole sheet surface). Wrapped content in a ScrollView with .scrollBounceBehavior(.basedOnSize) so large Dynamic Type can't clip the progress bar.
  • PropagationNodeManager.syncNow() — Resets transfer state up front so the sheet opens on a clean "Connecting…" state instead of briefly flashing the previous run's "Download complete / N new messages".
  • Live-update hardening — The sheet is presented through a small container that reads syncState in a real view body, so SwiftUI Observation reliably drives live progress + the terminal result (rather than relying solely on observation inside the .sheet content closure).

Behavior notes

  • A no-propagation-node tap now surfaces the .noPath error in the sheet — previously that case showed no feedback at all.

Verification

  • Builds clean: Columba-Swift / Debug-Swift, iPhone 17 simulator (BUILD SUCCEEDED).
  • Diff reviewed across regression + SwiftUI-correctness lenses; no unit tests exercise the changed paths (UI/presentation + one state reset).

🤖 Generated with Claude Code

The propagation-node sync status sheet popped up automatically on every
sync, including background, periodic, and on-foreground auto-syncs — not
just user-initiated ones. It also rendered as an opaque content-sized band
floating inside the system's default glass sheet chrome.

- ChatsView: stop auto-presenting via `.onChange(of: syncState.isSyncing)`.
  The sheet is now shown only when the user taps a refresh button (toolbar
  and empty-state). Background / periodic / on-foreground / pull-to-refresh
  syncs update `syncState` silently.
- SyncStatusBottomSheet: color the whole sheet with `.presentationBackground`
  instead of `.background(...)` on the content (the source of the glass +
  opaque-band artifact). Wrap content in a scroll-safe container so large
  Dynamic Type can't clip the progress bar.
- PropagationNodeManager.syncNow: reset transfer state up front so the sheet
  opens on a clean "Connecting" state rather than flashing the prior run's
  result.
- Present the sheet through a small container that reads `syncState` in a real
  view body, so SwiftUI Observation reliably drives live progress + the
  terminal result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two issues with the propagation-node sync status sheet on the Chats screen: it was auto-appearing on every sync (background, periodic, foreground), and its background styling was visually broken (painting an opaque band inside the glass chrome).

  • ChatsView.swift: Removes the .onChange-driven auto-presentation; the sheet is now explicitly presented only when the toolbar or empty-state refresh button is tapped. Adds SyncStatusSheetContainer so SwiftUI Observation reliably re-renders the sheet during live syncs.
  • SyncStatusBottomSheet.swift: Replaces content-level .background(...) with .presentationBackground(...) (iOS 16.4+ correct API), and wraps content in a ScrollView with .scrollBounceBehavior(.basedOnSize) to handle large Dynamic Type without clipping the progress bar.
  • PropagationNodeManager.swift: Adds userInitiated: Bool = false to syncNow() and gates the up-front syncState reset on that flag, so background/periodic syncs never clobber an open sheet.

Confidence Score: 5/5

Safe to merge — the two root-cause bugs (auto-presentation on every sync, broken sheet background) are correctly fixed, and the userInitiated guard properly insulates background/periodic syncs from mutating an open sheet.

The three-pronged fix (explicit presentation gate, userInitiated state-reset flag, and presentationBackground swap) is consistent across all call sites. The SyncStatusSheetContainer correctly bridges @Observable observation into the sheet content. All changed callers pass the right value for userInitiated. No new state-sharing or race paths are introduced beyond what was already flagged in prior review threads.

No files require special attention. The pull-to-refresh userInitiated: true concern was already captured in a prior review thread and is not new to this diff.

Important Files Changed

Filename Overview
Sources/ColumbaApp/Services/PropagationNodeManager.swift Adds userInitiated: Bool = false parameter to syncNow(); gates the syncState reset on that flag so background/periodic callers no longer clobber an open sheet.
Sources/ColumbaApp/Views/Chats/ChatsView.swift Removes .onChange-driven auto-presentation; toolbar and empty-state buttons now set isSyncSheetPresented = true directly. Adds SyncStatusSheetContainer for reliable Observation-driven sheet updates. Pull-to-refresh still passes userInitiated: true (noted in a prior review thread).
Sources/ColumbaApp/Views/Components/SyncStatusBottomSheet.swift Replaces content .background(...) with .presentationBackground(...) and wraps content in a ScrollView with .scrollBounceBehavior(.basedOnSize) for Dynamic Type support — both changes are correct for iOS 17+ target.
Sources/ColumbaApp/ViewModels/SettingsViewModel.swift One-line update: syncNow()syncNow(userInitiated: true) for the Settings-level "Sync Now" action, which is always user-initiated.
Sources/ColumbaApp/Views/Messaging/MessagingView.swift One-line update: syncNow()syncNow(userInitiated: true) for the in-conversation refresh button, correct since the user is explicitly tapping it.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    actor User
    participant ChatsView
    participant PropagationNodeManager
    participant SyncStatusSheetContainer
    participant SyncStatusBottomSheet

    Note over ChatsView: Toolbar / empty-state refresh button tapped
    User->>ChatsView: Tap refresh button
    ChatsView->>ChatsView: "isSyncSheetPresented = true"
    ChatsView->>SyncStatusSheetContainer: Present sheet
    SyncStatusSheetContainer->>SyncStatusBottomSheet: "state = PropagationTransferState()"

    ChatsView->>PropagationNodeManager: syncNow(userInitiated: true)
    PropagationNodeManager->>PropagationNodeManager: "syncState = .linking (reset)"
    SyncStatusSheetContainer-->>SyncStatusBottomSheet: Observe re-render (.linking)

    PropagationNodeManager->>PropagationNodeManager: propagationSync(timeout: 60s)
    PropagationNodeManager-->>SyncStatusSheetContainer: syncState updates (progress)
    SyncStatusSheetContainer-->>SyncStatusBottomSheet: Live re-render

    PropagationNodeManager->>PropagationNodeManager: "syncState = .complete / .noPath / .failed"
    SyncStatusSheetContainer-->>SyncStatusBottomSheet: Terminal result displayed
    User->>ChatsView: Drag sheet down to dismiss

    Note over ChatsView: Background / periodic / foreground auto-sync
    PropagationNodeManager->>PropagationNodeManager: syncNow(userInitiated: false)
    Note over PropagationNodeManager: syncState NOT reset — sheet unaffected
    PropagationNodeManager->>PropagationNodeManager: Silent sync (no sheet interaction)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    actor User
    participant ChatsView
    participant PropagationNodeManager
    participant SyncStatusSheetContainer
    participant SyncStatusBottomSheet

    Note over ChatsView: Toolbar / empty-state refresh button tapped
    User->>ChatsView: Tap refresh button
    ChatsView->>ChatsView: "isSyncSheetPresented = true"
    ChatsView->>SyncStatusSheetContainer: Present sheet
    SyncStatusSheetContainer->>SyncStatusBottomSheet: "state = PropagationTransferState()"

    ChatsView->>PropagationNodeManager: syncNow(userInitiated: true)
    PropagationNodeManager->>PropagationNodeManager: "syncState = .linking (reset)"
    SyncStatusSheetContainer-->>SyncStatusBottomSheet: Observe re-render (.linking)

    PropagationNodeManager->>PropagationNodeManager: propagationSync(timeout: 60s)
    PropagationNodeManager-->>SyncStatusSheetContainer: syncState updates (progress)
    SyncStatusSheetContainer-->>SyncStatusBottomSheet: Live re-render

    PropagationNodeManager->>PropagationNodeManager: "syncState = .complete / .noPath / .failed"
    SyncStatusSheetContainer-->>SyncStatusBottomSheet: Terminal result displayed
    User->>ChatsView: Drag sheet down to dismiss

    Note over ChatsView: Background / periodic / foreground auto-sync
    PropagationNodeManager->>PropagationNodeManager: syncNow(userInitiated: false)
    Note over PropagationNodeManager: syncState NOT reset — sheet unaffected
    PropagationNodeManager->>PropagationNodeManager: Silent sync (no sheet interaction)
Loading

Reviews (2): Last reviewed commit: "fix(ios): gate sync-state reset to user-..." | Re-trigger Greptile

Comment thread Sources/ColumbaApp/Services/PropagationNodeManager.swift Outdated
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ration 1)

Addresses Greptile P2: `syncNow()` reset `syncState` to `.linking` for every
caller, so a background / periodic / on-foreground sync could clobber a status
sheet the user had left open on a prior result (e.g. snap "3 new messages" back
to "Connecting"). The reset only matters for syncs that present the sheet.

Add `syncNow(userInitiated:)` (default false); reset the displayed transfer
state only when true. Pass true from the user-initiated paths (Chats refresh
button, pull-to-refresh, empty-state refresh, Settings Sync Now, Messaging Sync
Messages); leave background callers (on-foreground auto-sync, BGAppRefresh,
--auto-sync debug, periodic task, test hook) on the default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@torlando-tech torlando-tech merged commit 608394e into main Jun 20, 2026
1 check passed
@torlando-tech torlando-tech deleted the fix/sync-status-sheet-manual-only branch June 20, 2026 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant