Go back to using one List for everything in compose
This commit is contained in:
parent
381f3ee737
commit
57990f8339
|
@ -131,7 +131,6 @@ class AttachmentsListController: ViewController {
|
|||
@EnvironmentObject private var controller: AttachmentsListController
|
||||
@EnvironmentObject private var draft: Draft
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
var body: some View {
|
||||
attachmentsList
|
||||
|
@ -217,7 +216,7 @@ fileprivate extension View {
|
|||
}
|
||||
|
||||
@available(visionOS 1.0, *)
|
||||
fileprivate struct AttachmentButtonLabelStyle: LabelStyle {
|
||||
struct AttachmentButtonLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
DefaultLabelStyle().makeBody(configuration: configuration)
|
||||
.foregroundStyle(.white)
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
//
|
||||
// AttachmentsListSection.swift
|
||||
// ComposeUI
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import InstanceFeatures
|
||||
import PencilKit
|
||||
|
||||
struct AttachmentsListSection: View {
|
||||
@ObservedObject var draft: Draft
|
||||
@ObservedObject var instanceFeatures: InstanceFeatures
|
||||
@Environment(\.canAddAttachment) private var canAddAttachment
|
||||
|
||||
var body: some View {
|
||||
attachmentRows
|
||||
|
||||
buttons
|
||||
.foregroundStyle(.tint)
|
||||
#if os(visionOS)
|
||||
.buttonStyle(.bordered)
|
||||
.labelStyle(AttachmentButtonLabelStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
private var attachmentRows: some View {
|
||||
ForEach(draft.draftAttachments) { attachment in
|
||||
AttachmentRowView(attachment: attachment)
|
||||
}
|
||||
.onMove(perform: moveAttachments)
|
||||
.onDelete(perform: deleteAttachments)
|
||||
.onInsert(of: DraftAttachment.readableTypeIdentifiersForItemProvider) { offset, providers in
|
||||
Self.insertAttachments(in: draft, at: offset, itemProviders: providers)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var buttons: some View {
|
||||
AddPhotoButton(addAttachments: self.addAttachments)
|
||||
|
||||
AddDrawingButton(draft: draft)
|
||||
|
||||
TogglePollButton(draft: draft)
|
||||
}
|
||||
|
||||
static func insertAttachments(in draft: Draft, at index: Int, itemProviders: [NSItemProvider]) {
|
||||
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
|
||||
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
|
||||
guard let attachment = object as? DraftAttachment else { return }
|
||||
DispatchQueue.main.async {
|
||||
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
||||
attachment.draft = draft
|
||||
draft.attachments.add(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addAttachments(itemProviders: [NSItemProvider]) {
|
||||
Self.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: itemProviders)
|
||||
}
|
||||
|
||||
private func moveAttachments(from source: IndexSet, to destination: Int) {
|
||||
// just using moveObjects(at:to:) on the draft.attachments NSMutableOrderedSet
|
||||
// results in the order switching back to the previous order and then to the correct one
|
||||
// on the subsequent 2 view updates. creating a new set with the proper order doesn't have that problem
|
||||
var array = draft.draftAttachments
|
||||
array.move(fromOffsets: source, toOffset: destination)
|
||||
draft.attachments = NSMutableOrderedSet(array: array)
|
||||
}
|
||||
|
||||
private func deleteAttachments(at indices: IndexSet) {
|
||||
var array = draft.draftAttachments
|
||||
array.remove(atOffsets: indices)
|
||||
draft.attachments = NSMutableOrderedSet(array: array)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct AddPhotoButton: View {
|
||||
let addAttachments: ([NSItemProvider]) -> Void
|
||||
@Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker
|
||||
|
||||
var body: some View {
|
||||
if let presentAssetPicker {
|
||||
Button("Add photo or video", systemImage: "photo") {
|
||||
presentAssetPicker { results in
|
||||
addAttachments(results.map(\.itemProvider))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AddDrawingButton: View {
|
||||
let draft: Draft
|
||||
@Environment(\.composeUIConfig.presentDrawing) private var presentDrawing
|
||||
|
||||
var body: some View {
|
||||
if let presentDrawing {
|
||||
Button("Add drawing", systemImage: "hand.draw") {
|
||||
presentDrawing(PKDrawing()) { drawing in
|
||||
let attachment = DraftAttachment(context: DraftsPersistentContainer.shared.viewContext)
|
||||
attachment.id = UUID()
|
||||
attachment.drawing = drawing
|
||||
attachment.draft = self.draft
|
||||
self.draft.attachments.add(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TogglePollButton: View {
|
||||
@ObservedObject var draft: Draft
|
||||
|
||||
var body: some View {
|
||||
Button(draft.poll == nil ? "Add a poll" : "Remove poll", systemImage: "chart.bar.doc.horizontal") {
|
||||
withAnimation {
|
||||
draft.poll = draft.poll == nil ? Poll(context: DraftsPersistentContainer.shared.viewContext) : nil
|
||||
}
|
||||
}
|
||||
.disabled(draft.attachments.count > 0)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddAttachmentConditionsModifier: ViewModifier {
|
||||
@ObservedObject var draft: Draft
|
||||
@ObservedObject var instanceFeatures: InstanceFeatures
|
||||
|
||||
private var canAddAttachment: Bool {
|
||||
if instanceFeatures.mastodonAttachmentRestrictions {
|
||||
return draft.attachments.count < 4
|
||||
&& draft.draftAttachments.allSatisfy { $0.type == .image }
|
||||
&& draft.poll == nil
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.environment(\.canAddAttachment, canAddAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
private struct CanAddAttachmentKey: EnvironmentKey {
|
||||
static let defaultValue = false
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var canAddAttachment: Bool {
|
||||
get { self[CanAddAttachmentKey.self] }
|
||||
set { self[CanAddAttachmentKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
struct DropAttachmentModifier: ViewModifier {
|
||||
let draft: Draft
|
||||
@Environment(\.canAddAttachment) private var canAddAttachment
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onDrop(of: DraftAttachment.readableTypeIdentifiersForItemProvider, delegate: AttachmentDropDelegate(draft: draft, canAddAttachment: canAddAttachment))
|
||||
}
|
||||
}
|
||||
|
||||
private struct AttachmentDropDelegate: DropDelegate {
|
||||
let draft: Draft
|
||||
let canAddAttachment: Bool
|
||||
|
||||
func validateDrop(info: DropInfo) -> Bool {
|
||||
canAddAttachment
|
||||
}
|
||||
|
||||
func performDrop(info: DropInfo) -> Bool {
|
||||
AttachmentsListSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: info.itemProviders(for: DraftAttachment.readableTypeIdentifiersForItemProvider))
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,351 +0,0 @@
|
|||
//
|
||||
// AttachmentsListView.swift
|
||||
// ComposeUI
|
||||
//
|
||||
// Created by Shadowfacts on 8/16/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import InstanceFeatures
|
||||
import CoreData
|
||||
import PhotosUI
|
||||
|
||||
struct AttachmentsListView: View {
|
||||
@ObservedObject var draft: Draft
|
||||
@ObservedObject var instanceFeatures: InstanceFeatures
|
||||
@Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker
|
||||
|
||||
private var canAddAttachment: Bool {
|
||||
if instanceFeatures.mastodonAttachmentRestrictions {
|
||||
return draft.attachments.count < 4 && draft.draftAttachments.allSatisfy { $0.type == .image } && draft.poll == nil
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private var callbacks: Callbacks {
|
||||
Callbacks(draft: draft, presentAssetPicker: presentAssetPicker)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
WrappedCollectionView(attachments: draft.draftAttachments, hasPoll: draft.poll != nil, callbacks: callbacks, canAddAttachment: canAddAttachment)
|
||||
// Impose a minimum height, because otherwise it defaults to zero which prevents the collection
|
||||
// view from laying out, and leaving the intrinsic content size at zero too.
|
||||
.frame(minHeight: 50)
|
||||
.padding(.horizontal, -8)
|
||||
.environmentObject(instanceFeatures)
|
||||
}
|
||||
}
|
||||
|
||||
private struct Callbacks {
|
||||
let draft: Draft
|
||||
let presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)?
|
||||
|
||||
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) {
|
||||
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
|
||||
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
|
||||
guard let attachment = object as? DraftAttachment else { return }
|
||||
DispatchQueue.main.async {
|
||||
// guard self.canAddAttachment else {
|
||||
// return
|
||||
// }
|
||||
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
||||
attachment.draft = self.draft
|
||||
self.draft.attachments.add(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeAttachment(at index: Int) {
|
||||
var array = draft.draftAttachments
|
||||
array.remove(at: index)
|
||||
draft.attachments = NSMutableOrderedSet(array: array)
|
||||
}
|
||||
|
||||
func reorderAttachments(with difference: CollectionDifference<DraftAttachment>) {
|
||||
let array = draft.draftAttachments.applying(difference)!
|
||||
draft.attachments = NSMutableOrderedSet(array: array)
|
||||
}
|
||||
|
||||
func addPhoto() {
|
||||
presentAssetPicker?() {
|
||||
insertAttachments(at: draft.attachments.count, itemProviders: $0.map(\.itemProvider))
|
||||
}
|
||||
}
|
||||
|
||||
func addDrawing() {
|
||||
}
|
||||
|
||||
func togglePoll() {
|
||||
draft.poll = draft.poll == nil ? Poll(context: DraftsPersistentContainer.shared.viewContext) : nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
private struct WrappedCollectionView: UIViewRepresentable {
|
||||
let attachments: [DraftAttachment]
|
||||
let hasPoll: Bool
|
||||
let callbacks: Callbacks
|
||||
let canAddAttachment: Bool
|
||||
|
||||
func makeUIView(context: Context) -> UICollectionView {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
config.trailingSwipeActionsConfigurationProvider = { indexPath in
|
||||
context.coordinator.trailingSwipeActions(for: indexPath)
|
||||
}
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
let view = IntrinsicContentSizeCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
context.coordinator.setHeightOfCellBeingDeleted = {
|
||||
view.heightOfCellBeingDeleted = $0
|
||||
}
|
||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: view) { collectionView, indexPath, itemIdentifier in
|
||||
context.coordinator.makeCell(collectionView: collectionView, indexPath: indexPath, item: itemIdentifier)
|
||||
}
|
||||
view.dataSource = dataSource
|
||||
context.coordinator.dataSource = dataSource
|
||||
view.delegate = context.coordinator
|
||||
view.isScrollEnabled = false
|
||||
|
||||
dataSource.reorderingHandlers.canReorderItem = {
|
||||
if case .attachment(_) = $0 {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
dataSource.reorderingHandlers.didReorder = { transaction in
|
||||
let attachmentsChanges = transaction.difference.map {
|
||||
switch $0 {
|
||||
case .insert(let offset, let element, let associatedWith):
|
||||
guard case .attachment(let attachment) = element else { fatalError() }
|
||||
return CollectionDifference<DraftAttachment>.Change.insert(offset: offset, element: attachment, associatedWith: associatedWith)
|
||||
case .remove(let offset, let element, let associatedWith):
|
||||
guard case .attachment(let attachment) = element else { fatalError() }
|
||||
return CollectionDifference<DraftAttachment>.Change.remove(offset: offset, element: attachment, associatedWith: associatedWith)
|
||||
}
|
||||
}
|
||||
let attachmentsDiff = CollectionDifference(attachmentsChanges)!
|
||||
callbacks.reorderAttachments(with: attachmentsDiff)
|
||||
}
|
||||
let longPressRecognizer = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(WrappedCollectionViewCoordinator.reorderingLongPressRecognized))
|
||||
longPressRecognizer.delegate = context.coordinator
|
||||
view.addGestureRecognizer(longPressRecognizer)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UICollectionView, context: Context) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.attachments, .buttons])
|
||||
snapshot.appendItems(attachments.map {
|
||||
.attachment($0)
|
||||
}, toSection: .attachments)
|
||||
snapshot.appendItems([
|
||||
.button(.addPhoto, enabled: canAddAttachment),
|
||||
.button(.addDrawing, enabled: canAddAttachment),
|
||||
.button(.togglePoll(adding: !hasPoll), enabled: true)
|
||||
], toSection: .buttons)
|
||||
context.coordinator.dataSource.apply(snapshot)
|
||||
context.coordinator.callbacks = callbacks
|
||||
}
|
||||
|
||||
func makeCoordinator() -> WrappedCollectionViewCoordinator {
|
||||
WrappedCollectionViewCoordinator(callbacks: callbacks)
|
||||
}
|
||||
|
||||
enum Section: Hashable {
|
||||
case attachments, buttons
|
||||
}
|
||||
|
||||
enum Item: Hashable {
|
||||
case attachment(DraftAttachment)
|
||||
case button(Button, enabled: Bool)
|
||||
|
||||
static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.attachment(a), .attachment(b)):
|
||||
return a.objectID == b.objectID
|
||||
case let (.button(a, aEnabled), .button(b, bEnabled)):
|
||||
return a == b && aEnabled == bEnabled
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .attachment(let draftAttachment):
|
||||
hasher.combine(0)
|
||||
hasher.combine(draftAttachment.objectID)
|
||||
case .button(let button, let enabled):
|
||||
hasher.combine(1)
|
||||
hasher.combine(button)
|
||||
hasher.combine(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Button: Hashable {
|
||||
case addPhoto
|
||||
case addDrawing
|
||||
case togglePoll(adding: Bool)
|
||||
}
|
||||
}
|
||||
|
||||
private final class IntrinsicContentSizeCollectionView: UICollectionView {
|
||||
private var _intrinsicContentSize = CGSize.zero
|
||||
// This hack is necessary because the content size changes at the beginning of the cell delete animation,
|
||||
// resulting in the bottommost cell being clipped.
|
||||
var heightOfCellBeingDeleted: CGFloat = 0 {
|
||||
didSet {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
var size = _intrinsicContentSize
|
||||
size.height += heightOfCellBeingDeleted
|
||||
return size
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if contentSize != _intrinsicContentSize {
|
||||
_intrinsicContentSize = contentSize
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
private final class WrappedCollectionViewCoordinator: NSObject, UICollectionViewDelegate, UIGestureRecognizerDelegate {
|
||||
var callbacks: Callbacks
|
||||
var setHeightOfCellBeingDeleted: ((CGFloat) -> Void)?
|
||||
|
||||
var dataSource: UICollectionViewDiffableDataSource<WrappedCollectionView.Section, WrappedCollectionView.Item>!
|
||||
|
||||
init(callbacks: Callbacks) {
|
||||
self.callbacks = callbacks
|
||||
}
|
||||
|
||||
private let attachmentCell = UICollectionView.CellRegistration<UICollectionViewListCell, DraftAttachment> { cell, indexPath, item in
|
||||
cell.contentConfiguration = UIHostingConfiguration {
|
||||
AttachmentRowView(attachment: item)
|
||||
}
|
||||
}
|
||||
|
||||
private let buttonCell = UICollectionView.CellRegistration<UICollectionViewListCell, (WrappedCollectionView.Button, Bool)> { cell, indexPath, item in
|
||||
var config = cell.defaultContentConfiguration()
|
||||
switch item.0 {
|
||||
case .addPhoto:
|
||||
config.image = UIImage(systemName: "photo")
|
||||
config.text = "Add photo or video"
|
||||
case .addDrawing:
|
||||
config.image = UIImage(systemName: "hand.draw")
|
||||
config.text = "Add drawing"
|
||||
case .togglePoll(let adding):
|
||||
config.image = UIImage(systemName: "chart.bar.doc.horizontal")
|
||||
config.text = adding ? "Add a poll" : "Remove poll"
|
||||
}
|
||||
config.textProperties.color = .tintColor
|
||||
if !item.1 {
|
||||
config.textProperties.colorTransformer = .monochromeTint
|
||||
config.imageProperties.tintColorTransformer = .monochromeTint
|
||||
}
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
|
||||
func makeCell(collectionView: UICollectionView, indexPath: IndexPath, item: WrappedCollectionView.Item) -> UICollectionViewCell {
|
||||
switch item {
|
||||
case .attachment(let attachment):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: attachmentCell, for: indexPath, item: attachment)
|
||||
case .button(let button, let enabled):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: buttonCell, for: indexPath, item: (button, enabled))
|
||||
}
|
||||
}
|
||||
|
||||
func trailingSwipeActions(for indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
guard case .attachment(let attachment) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [
|
||||
UIContextualAction(style: .destructive, title: "Delete", handler: { _, view, completion in
|
||||
self.setHeightOfCellBeingDeleted?(view.bounds.height)
|
||||
// Actually remove the attachment immediately, so that (potentially) the buttons enabling animates.
|
||||
self.callbacks.removeAttachment(at: indexPath.row)
|
||||
// Also manually apply a snapshot removing the attachment item, otherwise the delete swipe action animation is messed up.
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteItems([.attachment(attachment)])
|
||||
self.dataSource.apply(snapshot) {
|
||||
self.setHeightOfCellBeingDeleted?(0)
|
||||
completion(true)
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
switch dataSource.itemIdentifier(for: indexPath)! {
|
||||
case .attachment:
|
||||
return false
|
||||
case .button(_, let enabled):
|
||||
return enabled
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
collectionView.deselectItem(at: indexPath, animated: false)
|
||||
guard case .button(let button, _) = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
switch button {
|
||||
case .addPhoto:
|
||||
callbacks.addPhoto()
|
||||
case .addDrawing:
|
||||
callbacks.addDrawing()
|
||||
case .togglePoll:
|
||||
callbacks.togglePoll()
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveOfItemFromOriginalIndexPath originalIndexPath: IndexPath, atCurrentIndexPath currentIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
|
||||
let snapshot = dataSource.snapshot()
|
||||
let attachmentsSection = snapshot.indexOfSection(.attachments)!
|
||||
if proposedIndexPath.section != attachmentsSection {
|
||||
return IndexPath(item: snapshot.itemIdentifiers(inSection: .attachments).count - 1, section: attachmentsSection)
|
||||
} else {
|
||||
return proposedIndexPath
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let collectionView = gestureRecognizer.view as! UICollectionView
|
||||
let location = gestureRecognizer.location(in: collectionView)
|
||||
guard let indexPath = collectionView.indexPathForItem(at: location) else {
|
||||
return false
|
||||
}
|
||||
return collectionView.beginInteractiveMovementForItem(at: indexPath)
|
||||
}
|
||||
|
||||
@objc func reorderingLongPressRecognized(_ recognizer: UILongPressGestureRecognizer) {
|
||||
let collectionView = recognizer.view as! UICollectionView
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
break
|
||||
case .changed:
|
||||
collectionView.updateInteractiveMovementTargetPosition(recognizer.location(in: collectionView))
|
||||
case .ended:
|
||||
collectionView.endInteractiveMovement()
|
||||
case .cancelled:
|
||||
collectionView.cancelInteractiveMovement()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//#Preview {
|
||||
// AttachmentsListView()
|
||||
//}
|
|
@ -22,9 +22,10 @@ struct ComposeView: View {
|
|||
|
||||
private var navigationRoot: some View {
|
||||
ZStack {
|
||||
ScrollView(.vertical) {
|
||||
scrollContent
|
||||
List {
|
||||
listContent
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
#if !os(visionOS) && !targetEnvironment(macCatalyst)
|
||||
.modifier(ToolbarSafeAreaInsetModifier())
|
||||
|
@ -46,6 +47,9 @@ struct ComposeView: View {
|
|||
.ignoresSafeArea(.keyboard)
|
||||
})
|
||||
#endif
|
||||
// Have these after the overlays so they barely work instead of not working at all. FB11790805
|
||||
.modifier(DropAttachmentModifier(draft: draft))
|
||||
.modifier(AddAttachmentConditionsModifier(draft: draft, instanceFeatures: mastodonController.instanceFeatures))
|
||||
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
@ -63,19 +67,28 @@ struct ComposeView: View {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var scrollContent: some View {
|
||||
VStack(spacing: 4) {
|
||||
NewReplyStatusView(draft: draft, mastodonController: mastodonController)
|
||||
|
||||
NewHeaderView(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
||||
|
||||
ContentWarningTextField(draft: draft, focusedField: $focusedField)
|
||||
|
||||
NewMainTextView(value: $draft.text, focusedField: $focusedField)
|
||||
|
||||
AttachmentsListView(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
||||
}
|
||||
.padding(8)
|
||||
private var listContent: some View {
|
||||
NewReplyStatusView(draft: draft, mastodonController: mastodonController)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
NewHeaderView(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
ContentWarningTextField(draft: draft, focusedField: $focusedField)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
NewMainTextView(value: $draft.text, focusedField: $focusedField, handleAttachmentDrop: self.addAttachments)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
AttachmentsListSection(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
|
||||
}
|
||||
|
||||
private func addAttachments(_ itemProviders: [NSItemProvider]) {
|
||||
AttachmentsListSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: itemProviders)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +146,7 @@ private struct ToolbarActions: ToolbarContent {
|
|||
}
|
||||
|
||||
private var postButton: some View {
|
||||
// TODO: don't use the controller for this
|
||||
Button(action: controller.postStatus) {
|
||||
Text(draft.editedStatusID == nil ? "Post" : "Edit")
|
||||
}
|
||||
|
|
|
@ -12,10 +12,11 @@ struct NewMainTextView: View {
|
|||
|
||||
@Binding var value: String
|
||||
@FocusState.Binding var focusedField: FocusableField?
|
||||
var handleAttachmentDrop: ([NSItemProvider]) -> Void
|
||||
@State private var becomeFirstResponder = true
|
||||
|
||||
var body: some View {
|
||||
NewMainTextViewRepresentable(value: $value, becomeFirstResponder: $becomeFirstResponder)
|
||||
NewMainTextViewRepresentable(value: $value, becomeFirstResponder: $becomeFirstResponder, handleAttachmentDrop: handleAttachmentDrop)
|
||||
.focused($focusedField, equals: .body)
|
||||
.modifier(FocusedInputModifier())
|
||||
.overlay(alignment: .topLeading) {
|
||||
|
@ -29,6 +30,7 @@ struct NewMainTextView: View {
|
|||
private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||
@Binding var value: String
|
||||
@Binding var becomeFirstResponder: Bool
|
||||
var handleAttachmentDrop: ([NSItemProvider]) -> Void
|
||||
@Environment(\.composeInputBox) private var inputBox
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
@ -40,6 +42,7 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
|||
func makeUIView(context: Context) -> UITextView {
|
||||
// TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary
|
||||
let view = WrappedTextView(usingTextLayoutManager: true)
|
||||
view.addInteraction(UIDropInteraction(delegate: context.coordinator))
|
||||
view.delegate = context.coordinator
|
||||
view.adjustsFontForContentSizeCategory = true
|
||||
view.textContainer.lineBreakMode = .byWordWrapping
|
||||
|
@ -67,6 +70,7 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
|||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) {
|
||||
context.coordinator.value = $value
|
||||
context.coordinator.handleAttachmentDrop = handleAttachmentDrop
|
||||
context.coordinator.updateTextViewTextIfNecessary(value, textView: uiView)
|
||||
|
||||
uiView.isEditable = isEnabled
|
||||
|
@ -86,7 +90,7 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
|||
}
|
||||
|
||||
func makeCoordinator() -> WrappedTextViewCoordinator {
|
||||
let coordinator = WrappedTextViewCoordinator(value: $value)
|
||||
let coordinator = WrappedTextViewCoordinator(value: $value, handleAttachmentDrop: handleAttachmentDrop)
|
||||
// DispatchQueue.main.async {
|
||||
// inputBox.wrappedValue = coordinator
|
||||
// }
|
||||
|
@ -120,9 +124,11 @@ private final class WrappedTextViewCoordinator: NSObject {
|
|||
}()
|
||||
|
||||
var value: Binding<String>
|
||||
var handleAttachmentDrop: ([NSItemProvider]) -> Void
|
||||
|
||||
init(value: Binding<String>) {
|
||||
init(value: Binding<String>, handleAttachmentDrop: @escaping ([NSItemProvider]) -> Void) {
|
||||
self.value = value
|
||||
self.handleAttachmentDrop = handleAttachmentDrop
|
||||
}
|
||||
|
||||
private func plainTextFromAttributed(_ attributedText: NSAttributedString) -> String {
|
||||
|
@ -230,6 +236,21 @@ extension WrappedTextViewCoordinator: UITextViewDelegate {
|
|||
//
|
||||
//}
|
||||
|
||||
// Because of FB11790805, we can't handle drops for the entire screen.
|
||||
// The onDrop modifier also doesn't work when applied to the NewMainTextView.
|
||||
// So, manually add the UIInteraction to at least handle that.
|
||||
extension WrappedTextViewCoordinator: UIDropInteractionDelegate {
|
||||
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: any UIDropSession) -> Bool {
|
||||
session.canLoadObjects(ofClass: DraftAttachment.self)
|
||||
}
|
||||
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: any UIDropSession) -> UIDropProposal {
|
||||
UIDropProposal(operation: .copy)
|
||||
}
|
||||
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: any UIDropSession) {
|
||||
handleAttachmentDrop(session.items.map(\.itemProvider))
|
||||
}
|
||||
}
|
||||
|
||||
private final class WrappedTextView: UITextView {
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue