From e3acdfb599a2a3ac1a4d7824b1a70af54a825580 Mon Sep 17 00:00:00 2001 From: ecgang Date: Mon, 6 Jul 2026 10:35:31 -0700 Subject: [PATCH] fix: single showSidebar latch for launch list, drop one-shot flag (#1064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The launch conversation-list signal was a separate `AppChromeModel` one-shot, `presentsConversationListOnLaunch`, OR'd only into the people-sheet presentation binding. The competing root sheets/covers — the fingerprint sheet and the image pickers — gate on `!showSidebar && selectedPrivatePeerID == nil` and never subtracted that launch term, so when the launch flag raised the people sheet while `showSidebar` was still false, both the people sheet and (if `showingFingerprintFor != nil`) the fingerprint sheet evaluated their `isPresented` binding to true at once — a SwiftUI single-sheet conflict. Rather than thread the launch term through every competing binding, consume the launch signal once through the existing `showSidebar` latch. `showSidebar` moves from a `ContentView` `@State` local onto `AppChromeModel` (`ContentView` already observes it as an `@EnvironmentObject`, and `$appChromeModel.showSidebar` yields the same `Binding` the child views were already handed), so non-view launch code in `AppRuntime` can raise it directly. Every competing sheet already keys off `showSidebar`, so with the launch signal folded into that one latch there is no separate flag to reconcile and the collision cannot occur. `presentsConversationListOnLaunch` and its every reference are deleted. Behavior-preserving: `showSidebar` had a single owning `ContentView` instance, so promoting it to the shared chrome model does not change its lifetime or semantics. Co-Authored-By: Claude Opus 4.8 (1M context) --- bitchat/App/AppChromeModel.swift | 13 +++++++++---- bitchat/App/AppRuntime.swift | 2 +- bitchat/Views/ContentSheetViews.swift | 4 ++-- bitchat/Views/ContentView.swift | 24 ++++++++++++------------ 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/bitchat/App/AppChromeModel.swift b/bitchat/App/AppChromeModel.swift index ea4e86df..76611564 100644 --- a/bitchat/App/AppChromeModel.swift +++ b/bitchat/App/AppChromeModel.swift @@ -14,10 +14,15 @@ final class AppChromeModel: ObservableObject { @Published var bluetoothAlertMessage = "" @Published var bluetoothState: CBManagerState = .unknown @Published var showScreenshotPrivacyWarning = false - /// #1064: set once at launch when the last-active conversation resolves to - /// "present the conversation list" (first-ever launch or a stale DM peer). - /// `ContentView` folds this into the people-sheet presentation binding. - @Published var presentsConversationListOnLaunch = false + /// Latch for the people / conversation-list sheet. Owned here (rather than as + /// `ContentView` local `@State`) so non-view launch code can raise it: on launch + /// `AppRuntime` sets this to `true` when the last-active conversation resolves to + /// "present the conversation list" (first-ever launch or a stale/unrestorable DM + /// peer). `ContentView` binds the people sheet directly to this, and every + /// competing sheet/cover already gates on `!showSidebar`, so a single latch keeps + /// the launch presentation from colliding with the fingerprint / image-picker + /// sheets (#1064). + @Published var showSidebar = false private let chatViewModel: ChatViewModel private var cancellables = Set() diff --git a/bitchat/App/AppRuntime.swift b/bitchat/App/AppRuntime.swift index 846b6451..8c1f9ef2 100644 --- a/bitchat/App/AppRuntime.swift +++ b/bitchat/App/AppRuntime.swift @@ -161,7 +161,7 @@ final class AppRuntime: ObservableObject { // the public mesh timeline when a restore target existed but could not // be opened. if Self.shouldPresentConversationList(for: presentation, didOpenDirectChat: didOpenDirectChat) { - appChromeModel.presentsConversationListOnLaunch = true + appChromeModel.showSidebar = true } } diff --git a/bitchat/Views/ContentSheetViews.swift b/bitchat/Views/ContentSheetViews.swift index ac0c5b61..dd72d8b6 100644 --- a/bitchat/Views/ContentSheetViews.swift +++ b/bitchat/Views/ContentSheetViews.swift @@ -85,7 +85,7 @@ struct ContentPeopleSheetView: View { } } .navigationDestination(isPresented: Binding( - get: { appChromeModel.showingFingerprintFor != nil && (showSidebar || privateConversationModel.selectedPeerID != nil || appChromeModel.presentsConversationListOnLaunch) }, + get: { appChromeModel.showingFingerprintFor != nil && (showSidebar || privateConversationModel.selectedPeerID != nil) }, set: { isPresented in if !isPresented { appChromeModel.clearFingerprint() @@ -105,7 +105,7 @@ struct ContentPeopleSheetView: View { #endif #if os(iOS) .fullScreenCover(isPresented: Binding( - get: { showImagePicker && (showSidebar || privateConversationModel.selectedPeerID != nil || appChromeModel.presentsConversationListOnLaunch) }, + get: { showImagePicker && (showSidebar || privateConversationModel.selectedPeerID != nil) }, set: { newValue in if !newValue { showImagePicker = false diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 49d4bae8..6d3c16dd 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -41,7 +41,8 @@ struct ContentView: View { @FocusState private var isTextFieldFocused: Bool @Environment(\.colorScheme) var colorScheme @Environment(\.appTheme) private var appTheme - @State private var showSidebar = false + // `showSidebar` (the people/conversation-list sheet latch) lives on + // `AppChromeModel` so non-view launch code can raise it; see that property. @State private var selectedMessageSender: String? @State private var selectedMessageSenderID: PeerID? @FocusState private var isNicknameFieldFocused: Bool @@ -97,16 +98,15 @@ struct ContentView: View { #endif .onChange(of: selectedPrivatePeerID) { newValue in if newValue != nil { - showSidebar = true + appChromeModel.showSidebar = true } } .sheet( isPresented: Binding( - get: { showSidebar || selectedPrivatePeerID != nil || appChromeModel.presentsConversationListOnLaunch }, + get: { appChromeModel.showSidebar || selectedPrivatePeerID != nil }, set: { isPresented in if !isPresented { - showSidebar = false - appChromeModel.presentsConversationListOnLaunch = false + appChromeModel.showSidebar = false privateConversationModel.endConversation() } } @@ -114,7 +114,7 @@ struct ContentView: View { ) { #if os(iOS) ContentPeopleSheetView( - showSidebar: $showSidebar, + showSidebar: $appChromeModel.showSidebar, messageText: $messageText, selectedMessageSender: $selectedMessageSender, selectedMessageSenderID: $selectedMessageSenderID, @@ -132,7 +132,7 @@ struct ContentView: View { ) #else ContentPeopleSheetView( - showSidebar: $showSidebar, + showSidebar: $appChromeModel.showSidebar, messageText: $messageText, selectedMessageSender: $selectedMessageSender, selectedMessageSenderID: $selectedMessageSenderID, @@ -153,7 +153,7 @@ struct ContentView: View { AppInfoView() } .sheet(isPresented: Binding( - get: { appChromeModel.showingFingerprintFor != nil && !showSidebar && selectedPrivatePeerID == nil }, + get: { appChromeModel.showingFingerprintFor != nil && !appChromeModel.showSidebar && selectedPrivatePeerID == nil }, set: { _ in appChromeModel.clearFingerprint() } )) { if let peerID = appChromeModel.showingFingerprintFor { @@ -163,7 +163,7 @@ struct ContentView: View { } #if os(iOS) .fullScreenCover(isPresented: Binding( - get: { showImagePicker && !showSidebar && selectedPrivatePeerID == nil }, + get: { showImagePicker && !appChromeModel.showSidebar && selectedPrivatePeerID == nil }, set: { newValue in if !newValue { showImagePicker = false @@ -179,7 +179,7 @@ struct ContentView: View { #endif #if os(macOS) .sheet(isPresented: Binding( - get: { showMacImagePicker && !showSidebar && selectedPrivatePeerID == nil }, + get: { showMacImagePicker && !appChromeModel.showSidebar && selectedPrivatePeerID == nil }, set: { newValue in if !newValue { showMacImagePicker = false @@ -268,7 +268,7 @@ struct ContentView: View { private var headerView: some View { ContentHeaderView( - showSidebar: $showSidebar, + showSidebar: $appChromeModel.showSidebar, showVerifySheet: $showVerifySheet, showLocationNotes: $showLocationNotes, notesGeohash: $notesGeohash, @@ -289,7 +289,7 @@ struct ContentView: View { imagePreviewURL: $imagePreviewURL, windowCountPublic: $windowCountPublic, windowCountPrivate: $windowCountPrivate, - showSidebar: $showSidebar, + showSidebar: $appChromeModel.showSidebar, isTextFieldFocused: $isTextFieldFocused ) }