diff --git a/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift b/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift index f36afa06a..aaa04af40 100644 --- a/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift +++ b/Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift @@ -7,24 +7,11 @@ import SwiftUI import UIKit -import PhotosUI import FlipcashCore import FlipcashUI private let logger = Logger(label: "flipcash.attach-overlay") -/// What the panel and cards drawn over the keyboard hand back to the screen. -struct AttachOverlayActions { - var onCash: () -> Void - var onCamera: () -> Void - var onPhotos: () -> Void - var onCameraCapture: (ChatCameraCapture) -> Void - var onCameraCancel: () -> Void - var onPhotosAdd: ([PhotosPickerItem], ChatPhotoPreloader) -> Void - var onPhotosBack: () -> Void - var onAllPhotos: (() -> Void)? = nil -} - /// Draws the attach panel and its cards over the keyboard while the composer keeps focus, as /// ChatGPT does: in the keyboard's own window when ``KeyboardOverlayHost`` finds it, else over a /// keyboard-coloured input view swapped in under the field. @@ -59,7 +46,7 @@ final class AttachKeyboardOverlay { screen: ChatScreenViewController, items: [AttachMenuItem], composer: ComposerModel, - actions: AttachOverlayActions + actions: @escaping () -> AttachActions ) -> AttachOverlayMode { if let mode { return mode } let plan = AttachOverlayPlan( @@ -149,7 +136,7 @@ final class AttachKeyboardOverlay { /// Hands the photo card to the bar, with the keyboard down and the full picker opening: a sheet /// can't be presented from the keyboard's window. - func handOffToBar() { + private func handOffToBar() { guard mode != nil else { return } model.overKeyboard.opensLibraryInBar = true end() @@ -159,7 +146,7 @@ final class AttachKeyboardOverlay { private func makeContainer( appWindow: UIWindow, composer: ComposerModel, - actions: AttachOverlayActions + actions: @escaping () -> AttachActions ) -> AttachOverlayContainer { let container = AttachOverlayContainer() container.appWindow = appWindow @@ -177,7 +164,12 @@ final class AttachKeyboardOverlay { let root = AttachOverlayRoot( model: model, composer: composer, - actions: actions, + // 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() + actions.onAllPhotos = { self?.handOffToBar() } + return actions + }, report: { [weak container] region, rect in container?.hitRects[region] = rect }, onFinished: { [weak self] in self?.end() }, onAbort: { [weak self] in self?.abort() } @@ -321,7 +313,8 @@ private struct AttachOverlayRoot: View { let model: ConversationBarModel let composer: ComposerModel - let actions: AttachOverlayActions + /// Returns the screen's actions as of its latest render. + let actions: () -> AttachActions let report: (AttachOverlayRegion, CGRect?) -> Void let onFinished: () -> Void let onAbort: () -> Void @@ -340,7 +333,7 @@ private struct AttachOverlayRoot: View { landing: model.attachCard.landingChipFrame, menuPlacement: .straddlesPlus, selectionLimit: AttachMenuItem.photosSelectionLimit(attachedCount: composer.chips.count), - actions: actions, + actions: actions(), report: report ) } diff --git a/Flipcash/Core/Screens/Conversation/AttachSurface.swift b/Flipcash/Core/Screens/Conversation/AttachSurface.swift index 8e68d7234..c30180f1f 100644 --- a/Flipcash/Core/Screens/Conversation/AttachSurface.swift +++ b/Flipcash/Core/Screens/Conversation/AttachSurface.swift @@ -6,6 +6,7 @@ // import SwiftUI +import PhotosUI import FlipcashUI /// What the attach surface is showing. @@ -147,6 +148,20 @@ enum AttachSurfaceLayout { } } +/// What the attach menu and its cards hand back to the screen, built once and drawn by both the bar +/// and the overlay over the keyboard. +struct AttachActions { + var onCash: () -> Void = {} + var onCamera: () -> Void = {} + var onPhotos: () -> Void = {} + var onCameraCapture: (ChatCameraCapture) -> Void = { _ in } + var onCameraCancel: () -> Void = {} + var onPhotosAdd: ([PhotosPickerItem], ChatPhotoPreloader) -> Void = { _, _ in } + var onPhotosBack: () -> Void = {} + /// Replaces All Photos presenting the full picker from the card; nil presents it in place. + var onAllPhotos: (() -> Void)? = nil +} + /// The attach flow's one surface: a single glass shape that grows out of `+` into the menu, springs /// between the menu and the camera or photo card, and shrinks onto the chip a photo is staged as. /// @@ -164,7 +179,7 @@ struct AttachSurface: View { let landing: CGRect? let menuPlacement: AttachMenuPlacement let selectionLimit: Int - let actions: AttachOverlayActions + let actions: AttachActions /// Whether the photo card opens the full picker as it mounts, for one All Photos was handed to. var opensLibraryOnAppear = false /// Receives the regions that take touches as they change, and none once the surface is gone. diff --git a/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift b/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift index b2e5e6ceb..d40692ca3 100644 --- a/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift +++ b/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift @@ -298,16 +298,17 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { } private func bar(coordinator: Coordinator) -> AnyView { - coordinator.overlayActions = AttachOverlayActions( + let attachActions = AttachActions( onCash: onSendCash, onCamera: { [weak coordinator] in coordinator?.openCard(.camera) }, onPhotos: { [weak coordinator] in coordinator?.openCard(.photos) }, onCameraCapture: { [weak coordinator] capture in coordinator?.closeCard { onCameraCapture(capture) } }, + // Back into the menu, with the keyboard left where it was. onCameraCancel: { [model = barModel] in model.returnToMenu() }, onPhotosAdd: { [weak coordinator] items, preloader in coordinator?.closeCard { onPhotosAdded(items, preloader) } }, - onPhotosBack: { [model = barModel] in model.returnToMenu() }, - onAllPhotos: { [weak coordinator] in coordinator?.overlay.handOffToBar() } + onPhotosBack: { [model = barModel] in model.returnToMenu() } ) + coordinator.attachActions = attachActions return AnyView( ConversationBottomBar( showsSendCash: showsSendCash, @@ -327,27 +328,8 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { onAttachOpen: { [weak coordinator] items in coordinator?.openAttach(items: items) }, - onCamera: { [weak coordinator] in - coordinator?.openCard(.camera) - }, - onCameraCapture: { [weak coordinator] capture in - coordinator?.closeCard { onCameraCapture(capture) } - }, - onCameraCancel: { [model = barModel] in - // Back into the panel, with the keyboard left down. - model.returnToMenu() - }, - onPhotos: { [weak coordinator] in - coordinator?.openCard(.photos) - }, - onPhotosAdd: { [weak coordinator] items, preloader in - coordinator?.closeCard { onPhotosAdded(items, preloader) } - }, + attachActions: attachActions, onImagesDropped: onImagesDropped, - onPhotosBack: { [model = barModel] in - // Back into the panel, with the keyboard left down. - model.returnToMenu() - }, // Only a member can aim a reply, so the viewer may see what it quotes. quoteThumbnailLocation: { [resolver = coordinator.mediaURLResolver] kind in await resolver.thumbnailLocation(for: kind, canReact: true) @@ -384,8 +366,8 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { let composer: ComposerModel /// Draws the attach panel and cards over the keyboard while the composer keeps focus. let overlay: AttachKeyboardOverlay - /// What the panel and cards drawn over the keyboard hand back, as of the latest render. - var overlayActions: AttachOverlayActions? + /// What the attach menu and its cards hand back, as of the latest render. + var attachActions = AttachActions() init(model: ConversationBarModel, composer: ComposerModel) { self.model = model @@ -396,10 +378,15 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { /// `+` 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]) { - guard let screen, let overlayActions else { return } + 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, composer: composer, actions: overlayActions) + let mode = overlay.begin( + screen: screen, + items: items, + composer: composer, + actions: { [weak self] in self?.attachActions ?? AttachActions() } + ) switch mode { case .keyboardWindow, .inputView: break diff --git a/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift b/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift index 5ef5918c4..c74c55490 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift @@ -294,22 +294,11 @@ struct ConversationBottomBar: View { /// 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 } - /// Opens the camera from the attach menu. - var onCamera: () -> Void = {} - /// Receives a photo the inline camera took. - var onCameraCapture: (ChatCameraCapture) -> Void = { _ in } - /// Fired by the camera card's back chevron. - var onCameraCancel: () -> Void = {} - /// Opens the photo card from the attach menu. - var onPhotos: () -> Void = {} - /// Receives the photos added from the photo card, in the order they were selected, with the - /// loader already reading them. - var onPhotosAdd: ([PhotosPickerItem], ChatPhotoPreloader) -> Void = { _, _ in } + /// 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 /// from another app in split view. Only fired while the bar takes photos. var onImagesDropped: ([NSItemProvider]) -> Void = { _ in } - /// Fired by the photo card's back chevron and escape gesture. - var onPhotosBack: () -> Void = {} /// Where the reply strip's quoted photo loads its thumbnail from. var quoteThumbnailLocation: (ChatQuote.Kind) async -> ChatMediaLocation? = { _ in nil } @@ -658,15 +647,7 @@ struct ConversationBottomBar: View { landing: model.attachCard.landingChipFrame.map(toRow), menuPlacement: .standsOnPlus, selectionLimit: AttachMenuItem.photosSelectionLimit(attachedCount: composer.chips.count), - actions: AttachOverlayActions( - onCash: onSendCash, - onCamera: onCamera, - onPhotos: onPhotos, - onCameraCapture: onCameraCapture, - onCameraCancel: onCameraCancel, - onPhotosAdd: onPhotosAdd, - onPhotosBack: onPhotosBack - ), + actions: attachActions, opensLibraryOnAppear: model.overKeyboard.opensLibraryInBar ) { region, rect in if region == .card, let rect {