Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 12 additions & 19 deletions Flipcash/Core/Screens/Conversation/AttachKeyboardOverlay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<PhotosPickerItem>) -> 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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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() }
Expand Down Expand Up @@ -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
Expand All @@ -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
)
}
Expand Down
17 changes: 16 additions & 1 deletion Flipcash/Core/Screens/Conversation/AttachSurface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import PhotosUI
import FlipcashUI

/// What the attach surface is showing.
Expand Down Expand Up @@ -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<PhotosPickerItem>) -> 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.
///
Expand All @@ -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.
Expand Down
41 changes: 14 additions & 27 deletions Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
25 changes: 3 additions & 22 deletions Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<PhotosPickerItem>) -> 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 }

Expand Down Expand Up @@ -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 {
Expand Down
Loading