diff --git a/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift b/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift index aaa04af40..fc6319331 100644 --- a/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift +++ b/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift @@ -39,12 +39,12 @@ final class AttachKeyboardOverlay { self.locator = locator } - /// Plans how `+` opens the panel and, when it goes over the keyboard, puts the overlay up with - /// `items` as its rows. Returns the mode chosen; ``AttachOverlayMode/dismissKeyboard`` leaves the + /// Plans how `+` opens the panel and, when it goes over the keyboard, puts the overlay up. + /// `acceptsMedia` is whether the chat takes photos, as the bar's rows read it. Returns the mode chosen; ``AttachOverlayMode/dismissKeyboard`` leaves the /// caller to take the keyboard down. func begin( screen: ChatScreenViewController, - items: [AttachMenuItem], + acceptsMedia: Bool, composer: ComposerModel, actions: @escaping () -> AttachActions ) -> AttachOverlayMode { @@ -66,7 +66,7 @@ final class AttachKeyboardOverlay { // Before the switch: swapping the input view in announces the keyboard showing again, which // must not read as the keyboard taking a card's place. model.attachCard.closesOnKeyboardShow = false - let container = makeContainer(appWindow: appWindow, composer: composer, actions: actions) + let container = makeContainer(appWindow: appWindow, acceptsMedia: acceptsMedia, composer: composer, actions: actions) switch plan.mode { case .keyboardWindow: guard let window = plan.keyboardWindow else { return refuse() } @@ -87,7 +87,7 @@ final class AttachKeyboardOverlay { } mode = plan.mode - model.overKeyboard.activate(items: items) + model.overKeyboard.activate() // A tap on the transcript reaches the app's window, not the overlay, and would lower the // keyboard; while the menu is up it only closes the menu. screen.setTranscriptShield( @@ -145,6 +145,7 @@ final class AttachKeyboardOverlay { private func makeContainer( appWindow: UIWindow, + acceptsMedia: Bool, composer: ComposerModel, actions: @escaping () -> AttachActions ) -> AttachOverlayContainer { @@ -164,6 +165,7 @@ final class AttachKeyboardOverlay { let root = AttachOverlayRoot( model: model, composer: composer, + acceptsMedia: acceptsMedia, // A card here can't present a sheet, so All Photos hands the card to the bar instead. actions: { [weak self] in var actions = actions() @@ -313,6 +315,7 @@ private struct AttachOverlayRoot: View { let model: ConversationBarModel let composer: ComposerModel + let acceptsMedia: Bool /// Returns the screen's actions as of its latest render. let actions: () -> AttachActions let report: (AttachOverlayRegion, CGRect?) -> Void @@ -321,13 +324,18 @@ private struct AttachOverlayRoot: View { @State private var bounds: CGSize = .zero + // Read live, as the bar reads its own, so a photo staged while the menu is up updates both. + private var rows: ConversationBarBottomRow { + ConversationBarBottomRow(composer: composer, acceptsMedia: acceptsMedia) + } + var body: some View { ZStack(alignment: .topLeading) { Color.clear - if model.attachSurfaceIsMounted { + if model.attachSurfaceIsMounted, !rows.plusItems.isEmpty { AttachSurface( model: model, - items: model.overKeyboard.items, + items: rows.plusItems, plus: model.overKeyboard.plusFrame, card: AttachOverlayLayout.cardFrame(in: CGRect(origin: .zero, size: bounds), height: model.attachCard.height), landing: model.attachCard.landingChipFrame, diff --git a/Flipcash/Core/Screens/Conversation/AttachMenu.swift b/Flipcash/Core/Screens/Conversation/AttachMenu.swift index a39811645..65fb669a8 100644 --- a/Flipcash/Core/Screens/Conversation/AttachMenu.swift +++ b/Flipcash/Core/Screens/Conversation/AttachMenu.swift @@ -48,14 +48,14 @@ struct AttachMenu: View { var isStoodInFor = false /// Fired as the panel opens, before it is drawn: the keyboard goes down and the panel rides the bar /// down with it, or the panel goes up over the keyboard. - let onOpen: ([AttachMenuItem]) -> Void + let onOpen: () -> Void /// Receives `+`'s frame in window coordinates as it is laid out. var onPlusFrame: (CGRect) -> Void = { _ in } var body: some View { Button { if !panel.isOpen { - onOpen(items) + onOpen() } panel.animate { $0.toggle() } } label: { diff --git a/Flipcash/Core/Screens/Conversation/AttachOverKeyboard.swift b/Flipcash/Core/Screens/Conversation/AttachOverKeyboard.swift index 6cf1e36ae..61adf038c 100644 --- a/Flipcash/Core/Screens/Conversation/AttachOverKeyboard.swift +++ b/Flipcash/Core/Screens/Conversation/AttachOverKeyboard.swift @@ -75,9 +75,6 @@ final class AttachOverKeyboard { /// Whether the panel and cards are drawn over the keyboard rather than in the bar. private(set) var isActive = false - /// The menu rows the panel showed as it opened. - private(set) var items: [AttachMenuItem] = [] - /// `+`'s frame in window coordinates, as last laid out. var plusFrame: CGRect = .zero @@ -89,9 +86,8 @@ final class AttachOverKeyboard { /// Photos hands the card over to the keyboard-down flow, whose sheet the keyboard would cover. var opensLibraryInBar = false - /// Starts drawing over the keyboard, with `items` as the panel's rows. - func activate(items: [AttachMenuItem]) { - self.items = items + /// Starts drawing over the keyboard. + func activate() { isActive = true } diff --git a/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift b/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift index d40692ca3..92634988b 100644 --- a/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift +++ b/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift @@ -325,8 +325,8 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { isJoiningChat: isJoiningChat, mentions: mentions, acceptsMedia: acceptsMedia, - onAttachOpen: { [weak coordinator] items in - coordinator?.openAttach(items: items) + onAttachOpen: { [weak coordinator, acceptsMedia] in + coordinator?.openAttach(acceptsMedia: acceptsMedia) }, attachActions: attachActions, onImagesDropped: onImagesDropped, @@ -375,15 +375,15 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { self.overlay = AttachKeyboardOverlay(model: model) } - /// `+` is opening the panel with `items` as its rows: over the keyboard while it is up and its - /// window or the field's input view can carry the panel, else with the keyboard taken down. - func openAttach(items: [AttachMenuItem]) { + /// `+` is opening the panel: over the keyboard while it is up and its window or the field's + /// input view can carry the panel, else with the keyboard taken down. + func openAttach(acceptsMedia: Bool) { guard let screen else { return } // Sized now, so the card's content is laid out at its final size while the menu is up. model.attachCard.measure(screenHeight: screen.view.bounds.height) let mode = overlay.begin( screen: screen, - items: items, + acceptsMedia: acceptsMedia, composer: composer, actions: { [weak self] in self?.attachActions ?? AttachActions() } ) diff --git a/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift b/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift index c74c55490..90773f88e 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift @@ -236,6 +236,12 @@ struct ConversationBarBottomRow: Equatable { self.init(plusItems: AttachMenuItem.items(showsCash: false, acceptsMedia: acceptsMedia, attachedCount: attachedCount)) } + /// Returns the row for `composer` as it stands, in a chat where `acceptsMedia` says whether + /// photos may be sent. The bar and the overlay over the keyboard both read their rows here. + init(composer: ComposerModel, acceptsMedia: Bool) { + self.init(isEditing: composer.isEditing, acceptsMedia: acceptsMedia, attachedCount: composer.chips.count) + } + private init(plusItems: [AttachMenuItem]) { self.plusItems = plusItems } @@ -293,7 +299,7 @@ struct ConversationBottomBar: View { var acceptsMedia: Bool = false /// Fired as `+` opens the attach panel with the given rows: the panel goes up over the keyboard, /// or the keyboard goes down under it. - var onAttachOpen: ([AttachMenuItem]) -> Void = { _ in } + var onAttachOpen: () -> Void = {} /// What the attach menu and its cards hand back, the same value the overlay over the keyboard draws with. var attachActions = AttachActions() /// Receives images dropped on the bar, such as the system screenshot thumbnail or an image @@ -688,11 +694,7 @@ struct ConversationBottomBar: View { } private var bottomRow: ConversationBarBottomRow { - ConversationBarBottomRow( - isEditing: composer.isEditing, - acceptsMedia: acceptsMedia, - attachedCount: composer.chips.count - ) + ConversationBarBottomRow(composer: composer, acceptsMedia: acceptsMedia) } } @@ -1062,7 +1064,7 @@ struct ConversationComposer: View { /// Whether the attach surface is drawn in `+`'s place, which hides `+`. var plusIsStoodInFor = false /// Fired as `+` opens the attach panel with the given rows. - var onAttachOpen: ([AttachMenuItem]) -> Void = { _ in } + var onAttachOpen: () -> Void = {} /// Whether `$` stands beside the field and merges into it as the draft starts, so the send /// arrow waits for it to land instead of appearing over it. var sendWaitsForCashJoin = false diff --git a/FlipcashTests/Chat/AttachOverlayModeTests.swift b/FlipcashTests/Chat/AttachOverlayModeTests.swift index 02359f029..09b6b27d8 100644 --- a/FlipcashTests/Chat/AttachOverlayModeTests.swift +++ b/FlipcashTests/Chat/AttachOverlayModeTests.swift @@ -131,7 +131,7 @@ struct AttachOverlayModeTests { model.attachCard.open(.photos, screenHeight: 800) #expect(model.overflow != .none) - model.overKeyboard.activate(items: [.camera, .photos]) + model.overKeyboard.activate() #expect(model.overflow == .none) } diff --git a/FlipcashTests/Chat/AttachPanelTests.swift b/FlipcashTests/Chat/AttachPanelTests.swift index 4634660f0..23facc7aa 100644 --- a/FlipcashTests/Chat/AttachPanelTests.swift +++ b/FlipcashTests/Chat/AttachPanelTests.swift @@ -132,7 +132,7 @@ struct AttachPanelTests { @Test("Over the keyboard the bar claims no room, even with the panel up: the overlay takes its touches") func overKeyboardClaimsNoRoom() { let model = ConversationBarModel() - model.overKeyboard.activate(items: [.camera, .photos]) + model.overKeyboard.activate() model.attachPanel.open() #expect(model.overflow == .none) } diff --git a/FlipcashTests/Chat/ConversationBarLeadingControlTests.swift b/FlipcashTests/Chat/ConversationBarLeadingControlTests.swift index e8bfda284..a93e824f4 100644 --- a/FlipcashTests/Chat/ConversationBarLeadingControlTests.swift +++ b/FlipcashTests/Chat/ConversationBarLeadingControlTests.swift @@ -6,6 +6,8 @@ // import Testing +import UIKit +import FlipcashCore @testable import Flipcash @MainActor @@ -75,4 +77,18 @@ struct ConversationBarBottomRowTests { func hiddenWhileEditing() { #expect(row(isEditing: true).isEmpty) } + + @Test("Read from the composer, the rows follow photos staged after the menu opened") + func followsTheComposer() throws { + let composer = ComposerModel() + let uploader = ChatMediaUploader(blob: MockChatMediaBlobStore()) + #expect(ConversationBarBottomRow(composer: composer, acceptsMedia: true).plusItems == [.camera, .photos]) + + let image = UIGraphicsImageRenderer(size: CGSize(width: 4, height: 3)).image { _ in } + for _ in 0..