From b54113f0e49e54f4833140db0b850b5b798ae3c1 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Sat, 29 Aug 2026 02:17:03 +0700 Subject: [PATCH] #6 fix gallery preview after canceling a previous pick - Stage picked media only when the picker is dismissed with a confirmed selection instead of on every selection change, so canceling restores an empty binding and never schedules a preview. - Reset the selection binding after dismissal and recreate the picker via a reset token, so a stale selection can no longer leak into the next presentation and break its preview/send flow. - Add verify.sh invariants for the picker state machine. --- Scripts/verify.sh | 8 ++++++++ Sources/Shared/UI/ChatView.swift | 23 +++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Scripts/verify.sh b/Scripts/verify.sh index ad5e7c1..910f577 100755 --- a/Scripts/verify.sh +++ b/Scripts/verify.sh @@ -328,6 +328,14 @@ grep -q 'pendingGroupDeletion' Sources/Shared/UI/MainChatView.swift || { echo "Chat list must offer deleting group chats" exit 1 } +grep -q 'pickerResetToken' Sources/Shared/UI/ChatView.swift || { + echo "The photo picker must be recreated after each selection" + exit 1 +} +grep -q 'let items = pickedMediaItems' Sources/Shared/UI/ChatView.swift || { + echo "Gallery staging must run on picker dismissal only" + exit 1 +} grep -q 'ArchiveSyncCursorPolicy.requestPosition' Sources/Shared/XMPP/XMPPService.swift || { echo "MAM reconnects must prefer the durable archive UID over timestamps" exit 1 diff --git a/Sources/Shared/UI/ChatView.swift b/Sources/Shared/UI/ChatView.swift index 898b2bd..d974d3f 100644 --- a/Sources/Shared/UI/ChatView.swift +++ b/Sources/Shared/UI/ChatView.swift @@ -68,6 +68,7 @@ struct ChatView: View { @State private var captureAttemptID = UUID() @State private var captureStartTask: Task? @State private var captureSuspendsArchiveSync = false + @State private var pickerResetToken = UUID() @State private var timelineEntries: [ChatTimelineEntry] = [] /// Messages of this conversation straight from the SwiftData store, @@ -125,6 +126,7 @@ struct ChatView: View { _captureAttemptID = State(initialValue: UUID()) _captureStartTask = State?>(initialValue: nil) _captureSuspendsArchiveSync = State(initialValue: false) + _pickerResetToken = State(initialValue: UUID()) _timelineEntries = State<[ChatTimelineEntry]>(initialValue: []) } @@ -226,10 +228,20 @@ struct ChatView: View { matching: .any(of: [.images, .videos]), preferredItemEncoding: .current ) + // A fresh picker identity per attempt: clearing the selection while + // the picker is presented desynchronizes its internal state, and the + // stale selection then leaks into the next presentation. + .id(pickerResetToken) .onChange(of: showingMediaPicker) { _, isPresented in - if !isPresented { - schedulePendingAttachmentPreview() - } + guard !isPresented else { return } + // On Done the binding holds the chosen items; on Cancel SwiftUI + // restores the pre-presentation value (kept empty below), so + // staging runs only for a confirmed selection. + let items = pickedMediaItems + pickedMediaItems = [] + pickerResetToken = UUID() + guard !items.isEmpty else { return } + Task { await stagePickedMedia(items) } } #if os(iOS) .fullScreenCover( @@ -259,10 +271,6 @@ struct ChatView: View { .ignoresSafeArea() } #endif - .onChange(of: pickedMediaItems) { _, items in - guard !items.isEmpty else { return } - Task { await stagePickedMedia(items) } - } .onChange(of: draft) { _, text in model.updateComposerActivity(text, in: liveConversation) } @@ -1364,7 +1372,6 @@ struct ChatView: View { var sourceURLs: [URL] = [] var failedItemCount = 0 defer { - pickedMediaItems = [] isPreparingAttachments = false sourceURLs.forEach(discardPickedMediaFile) }