From 34dccf1f377221101f018dae97a47e1a0e2d8fd4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 14 Mar 2020 15:47:15 -0400 Subject: [PATCH] Extract compose attachments into separate VC --- Tusker.xcodeproj/project.pbxproj | 40 ++- Tusker/DraftsManager.swift | 15 +- .../AssetPickerViewController.swift | 8 +- .../AddAttachmentTableViewCell.swift | 34 +++ .../AddAttachmentTableViewCell.xib | 54 ++++ .../ComposeAttachmentTableViewCell.swift | 78 ++++++ .../ComposeAttachmentTableViewCell.xib | 82 ++++++ .../ComposeAttachmentsViewController.swift | 265 ++++++++++++++++++ .../Attachments/CompositionAttachment.swift | 25 ++ .../CompositionAttachmentData.swift | 185 ++++++++++++ .../Compose/ComposeViewController.swift | 194 +++---------- .../Screens/Compose/ComposeViewController.xib | 72 +---- .../Compose/CompositionAttachment.swift | 16 +- .../Compose/Drafts/DraftTableViewCell.swift | 2 +- .../Compose Media/ComposeMediaView.swift | 4 +- 15 files changed, 828 insertions(+), 246 deletions(-) create mode 100644 Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.swift create mode 100644 Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.xib create mode 100644 Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.swift create mode 100644 Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.xib create mode 100644 Tusker/Screens/Compose/Attachments/ComposeAttachmentsViewController.swift create mode 100644 Tusker/Screens/Compose/Attachments/CompositionAttachment.swift create mode 100644 Tusker/Screens/Compose/Attachments/CompositionAttachmentData.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 62858ba5..ee3d747a 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; }; + D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; }; D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; }; D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; @@ -76,7 +77,7 @@ D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; }; D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; }; - D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; }; + D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; }; D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; }; D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; }; D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493A23C1000300612E6E /* AlbumTableViewCell.swift */; }; @@ -109,6 +110,11 @@ D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; }; D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; }; + D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */; }; + D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */; }; + D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */; }; + D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */; }; + D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; }; D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; }; @@ -303,6 +309,7 @@ 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = ""; }; D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = ""; }; + D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsViewController.swift; sourceTree = ""; }; D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = ""; }; D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = ""; }; D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = ""; }; @@ -357,7 +364,7 @@ D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = ""; }; D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = ""; }; D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = ""; }; - D626493423BD94CE00612E6E /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = ""; }; + D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = ""; }; D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = ""; }; D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = ""; }; D626493A23C1000300612E6E /* AlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTableViewCell.swift; sourceTree = ""; }; @@ -389,6 +396,11 @@ D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = ""; }; + D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddAttachmentTableViewCell.xib; sourceTree = ""; }; + D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttachmentTableViewCell.swift; sourceTree = ""; }; + D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentTableViewCell.swift; sourceTree = ""; }; + D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeAttachmentTableViewCell.xib; sourceTree = ""; }; + D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = ""; }; D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = ""; }; D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = ""; }; @@ -779,6 +791,20 @@ path = Shortcuts; sourceTree = ""; }; + D63F9C64241C4CAA004C03CF /* Attachments */ = { + isa = PBXGroup; + children = ( + D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */, + D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */, + D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */, + D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */, + D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */, + D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */, + D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */, + ); + path = Attachments; + sourceTree = ""; + }; D641C780213DD7C4004B4513 /* Screens */ = { isa = PBXGroup; children = ( @@ -862,10 +888,10 @@ isa = PBXGroup; children = ( D6B053A023BD2BED00A066FA /* Asset Picker */, + D63F9C64241C4CAA004C03CF /* Attachments */, D627FF77217E94F200CC0648 /* Drafts */, D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */, D66362702136338600C9CBA2 /* ComposeViewController.swift */, - D626493423BD94CE00612E6E /* CompositionAttachment.swift */, D6285B5221EA708700FE4B39 /* StatusFormat.swift */, D620483123D2A6A3008A63EF /* CompositionState.swift */, ); @@ -1480,7 +1506,9 @@ D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */, D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */, 0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */, + D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */, D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, + D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */, D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */, @@ -1589,7 +1617,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */, + D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */, D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, @@ -1626,6 +1654,7 @@ D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */, D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */, + D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */, D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */, @@ -1647,6 +1676,7 @@ D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */, + D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */, D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */, @@ -1656,6 +1686,7 @@ D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */, D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */, D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, + D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */, D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */, D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, @@ -1684,6 +1715,7 @@ D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */, + D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, diff --git a/Tusker/DraftsManager.swift b/Tusker/DraftsManager.swift index 39d182ca..e1b40889 100644 --- a/Tusker/DraftsManager.swift +++ b/Tusker/DraftsManager.swift @@ -39,7 +39,7 @@ class DraftsManager: Codable { return drafts.sorted(by: { $0.lastModified > $1.lastModified }) } - func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment]) -> Draft { + func create(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [CompositionAttachment]) -> Draft { let draft = Draft(accountID: accountID, text: text, contentWarning: contentWarning, inReplyToID: inReplyToID, attachments: attachments) drafts.append(draft) return draft @@ -58,11 +58,11 @@ extension DraftsManager { private(set) var accountID: String private(set) var text: String private(set) var contentWarning: String? - private(set) var attachments: [DraftAttachment] + var attachments: [CompositionAttachment] private(set) var inReplyToID: String? private(set) var lastModified: Date - init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [DraftAttachment], lastModified: Date = Date()) { + init(accountID: String, text: String, contentWarning: String?, inReplyToID: String?, attachments: [CompositionAttachment], lastModified: Date = Date()) { self.id = UUID() self.accountID = accountID self.text = text @@ -71,8 +71,8 @@ extension DraftsManager { self.attachments = attachments self.lastModified = lastModified } - - func update(accountID: String, text: String, contentWarning: String?, attachments: [DraftAttachment]) { + + func update(accountID: String, text: String, contentWarning: String?, attachments: [CompositionAttachment]) { self.accountID = accountID self.text = text self.contentWarning = contentWarning @@ -84,9 +84,4 @@ extension DraftsManager { return lhs.id == rhs.id } } - - struct DraftAttachment: Codable { - let attachment: CompositionAttachment - let description: String - } } diff --git a/Tusker/Screens/Compose/Asset Picker/AssetPickerViewController.swift b/Tusker/Screens/Compose/Asset Picker/AssetPickerViewController.swift index 9b820261..c5732e73 100644 --- a/Tusker/Screens/Compose/Asset Picker/AssetPickerViewController.swift +++ b/Tusker/Screens/Compose/Asset Picker/AssetPickerViewController.swift @@ -10,15 +10,15 @@ import UIKit import Photos protocol AssetPickerViewControllerDelegate { - func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool - func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) + func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool + func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) } class AssetPickerViewController: UINavigationController { var assetPickerDelegate: AssetPickerViewControllerDelegate? - var currentCollectionSelectedAssets: [CompositionAttachment] { + var currentCollectionSelectedAssets: [CompositionAttachmentData] { if let vc = visibleViewController as? AssetCollectionViewController { return vc.selectedAssets.map { .asset($0) } } else { @@ -70,7 +70,7 @@ extension AssetPickerViewController: AssetCollectionViewControllerDelegate { extension AssetPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - let attachment: CompositionAttachment + let attachment: CompositionAttachmentData if let image = info[.originalImage] as? UIImage { attachment = .image(image) } else if let url = info[.mediaURL] as? URL { diff --git a/Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.swift b/Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.swift new file mode 100644 index 00000000..0c60b610 --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.swift @@ -0,0 +1,34 @@ +// +// AddAttachmentTableViewCell.swift +// Tusker +// +// Created by Shadowfacts on 3/13/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class AddAttachmentTableViewCell: UITableViewCell { + + @IBOutlet weak var iconImageView: UIImageView! + @IBOutlet weak var label: UILabel! + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + let imageName: String + if traitCollection.userInterfaceStyle == .dark { + imageName = "photo.fill" + } else { + imageName = "photo" + } + iconImageView.image = UIImage(systemName: imageName) + } + + func setEnabled(_ enabled: Bool) { + let color = enabled ? UIColor.systemBlue : .systemGray + iconImageView.tintColor = color + label.textColor = color + } + +} diff --git a/Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.xib b/Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.xib new file mode 100644 index 00000000..aa0328b0 --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/AddAttachmentTableViewCell.xib @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.swift b/Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.swift new file mode 100644 index 00000000..df8ab2e6 --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.swift @@ -0,0 +1,78 @@ +// +// ComposeAttachmentTableViewCell.swift +// Tusker +// +// Created by Shadowfacts on 3/13/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +//import Combine +import Photos +import AVFoundation + +protocol ComposeAttachmentTableViewCellDelegate: class { + func removeAttachment(_ cell: ComposeAttachmentTableViewCell) + func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell) +} + +class ComposeAttachmentTableViewCell: UITableViewCell { + + weak var delegate: ComposeAttachmentTableViewCellDelegate? + + @IBOutlet weak var assetImageView: UIImageView! + @IBOutlet weak var descriptionTextView: UITextView! + @IBOutlet weak var descriptionPlaceholderLabel: UILabel! + + var attachment: CompositionAttachment! + + override func awakeFromNib() { + super.awakeFromNib() + + assetImageView.layer.masksToBounds = true + assetImageView.layer.cornerRadius = 8 + + descriptionTextView.delegate = self + } + + func updateUI(for attachment: CompositionAttachment) { + self.attachment = attachment + + descriptionTextView.text = attachment.description + updateDescriptionPlaceholderLabel() + + switch attachment.data { + case let .image(image): + assetImageView.image = image + case let .asset(asset): + let size = CGSize(width: 80, height: 80) + PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in + guard self.attachment == attachment else { return } + self.assetImageView.image = image + } + case let .video(url): + let asset = AVURLAsset(url: url) + let imageGenerator = AVAssetImageGenerator(asset: asset) + if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) { + assetImageView.image = UIImage(cgImage: cgImage) + } + } + } + + func updateDescriptionPlaceholderLabel() { + descriptionPlaceholderLabel.isHidden = !descriptionTextView.text.isEmpty + } + + @IBAction func removeButtonPressed(_ sender: Any) { + delegate?.removeAttachment(self) + } + +} + +extension ComposeAttachmentTableViewCell: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + delegate?.attachmentDescriptionChanged(self) + attachment.description = textView.text + updateDescriptionPlaceholderLabel() + } +} diff --git a/Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.xib b/Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.xib new file mode 100644 index 00000000..65e006db --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/ComposeAttachmentTableViewCell.xib @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tusker/Screens/Compose/Attachments/ComposeAttachmentsViewController.swift b/Tusker/Screens/Compose/Attachments/ComposeAttachmentsViewController.swift new file mode 100644 index 00000000..a3e8b083 --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/ComposeAttachmentsViewController.swift @@ -0,0 +1,265 @@ +// +// ComposeAttachmentsViewController.swift +// Tusker +// +// Created by Shadowfacts on 3/11/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +protocol ComposeAttachmentsViewControllerDelegate: class { + func composeSelectedAttachmentsDidChange() + func composeRequiresAttachmentDescriptionsDidChange() +} + +class ComposeAttachmentsViewController: UITableViewController { + + weak var mastodonController: MastodonController! + weak var delegate: ComposeAttachmentsViewControllerDelegate? + + private var heightConstraint: NSLayoutConstraint! + + var attachments: [CompositionAttachment] = [] { + didSet { + delegate?.composeSelectedAttachmentsDidChange() + updateAddAttachmentsButtonEnabled() + } + } + + var requiresAttachmentDescriptions: Bool { + if Preferences.shared.requireAttachmentDescriptions { + return !attachments.allSatisfy { $0.description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + } else { + return false + } + } + + init(attachments: [CompositionAttachment], mastodonController: MastodonController) { + self.attachments = attachments + self.mastodonController = mastodonController + super.init(style: .plain) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 96 + + tableView.register(UINib(nibName: "AddAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "addAttachment") + tableView.register(UINib(nibName: "ComposeAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "composeAttachment") + + heightConstraint = tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height) + heightConstraint.isActive = true + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateHeightConstraint() + } + + func setAttachments(_ attachments: [CompositionAttachment]) { + self.attachments = attachments + tableView.reloadData() + updateHeightConstraint() + delegate?.composeRequiresAttachmentDescriptionsDidChange() + } + + private func updateHeightConstraint() { + heightConstraint.constant = tableView.contentSize.height + } + + private func isAddAttachmentsButtonEnabled() -> Bool { + switch mastodonController.instance.instanceType { + case .pleroma: + return true + case .mastodon: + return !attachments.contains(where: { $0.data.type == .video }) && attachments.count < 4 + } + } + + private func updateAddAttachmentsButtonEnabled() { + let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as! AddAttachmentTableViewCell + cell.setEnabled(isAddAttachmentsButtonEnabled()) + } + + func uploadAll(stepProgress: @escaping () -> Void, completion: @escaping (_ success: Bool, _ uploadedAttachments: [Attachment]) -> Void) { + let group = DispatchGroup() + + var anyFailed = false + var uploadedAttachments: [Result?] = [] + + for (index, compAttachment) in attachments.enumerated() { + group.enter() + + uploadedAttachments.append(nil) + + compAttachment.data.getData { (data, mimeType) in + stepProgress() + + let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file") + let request = Client.upload(attachment: formAttachment, description: compAttachment.description) + self.mastodonController.run(request) { (response) in + switch response { + case let .failure(error): + uploadedAttachments[index] = .failure(error) + anyFailed = true + case let .success(attachment, _): + uploadedAttachments[index] = .success(attachment) + } + + stepProgress() + group.leave() + } + } + } + + group.notify(queue: .main) { + if anyFailed { + let errors: [(Int, Error)] = uploadedAttachments.enumerated().compactMap { (index, result) in + switch result { + case let .failure(error): + return (index, error) + default: + return nil + } + + } + let title: String + var message: String + if errors.count == 1 { + title = NSLocalizedString("Could not upload attachment", comment: "single attachment upload failed alert title") + message = errors[0].1.localizedDescription + } else { + title = NSLocalizedString("Could not upload the following attachments", comment: "multiple attachment upload failures alert title") + message = "" + for (index, error) in errors { + message.append("Attachment \(index + 1): \(error.localizedDescription)") + } + } + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in + completion(false, []) + })) + } else { + let uploadedAttachments: [Attachment] = uploadedAttachments.compactMap { + switch $0 { + case let .success(attachment): + return attachment + default: + return nil + } + } + completion(true, uploadedAttachments) + } + } + } + + // MARK: Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + return attachments.count + case 1: + return 1 + default: + fatalError("invalid section \(section)") + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch indexPath.section { + case 0: + let attachment = attachments[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: "composeAttachment", for: indexPath) as! ComposeAttachmentTableViewCell + cell.delegate = self + cell.updateUI(for: attachment) + return cell + case 1: + let cell = tableView.dequeueReusableCell(withIdentifier: "addAttachment", for: indexPath) as! AddAttachmentTableViewCell + cell.setEnabled(isAddAttachmentsButtonEnabled()) + return cell + default: + fatalError("invalid section \(indexPath.section)") + } + } + + // MARK: Table view delegate + + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + if indexPath.section == 1, isAddAttachmentsButtonEnabled() { + return indexPath + } + return nil + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + if indexPath.section == 1 { + addAttachmentPressed() + } + } + + func addAttachmentPressed() { + let sheetContainer = AssetPickerSheetContainerViewController() + sheetContainer.assetPicker.assetPickerDelegate = self + present(sheetContainer, animated: true) + } + +} + +extension ComposeAttachmentsViewController: AssetPickerViewControllerDelegate { + func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool { + switch mastodonController.instance.instanceType { + case .pleroma: + return true + case .mastodon: + if (type == .video && attachments.count > 0) || + attachments.contains(where: { $0.data.type == .video }) || + assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) { + return false + } + return attachments.count + assetPicker.currentCollectionSelectedAssets.count < 4 + } + } + + func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) { + let attachments = attachments.map { + CompositionAttachment(data: $0) + } + let indexPaths = attachments.indices.map { IndexPath(row: $0 + self.attachments.count, section: 0) } + self.attachments.append(contentsOf: attachments) + tableView.insertRows(at: indexPaths, with: .automatic) + updateHeightConstraint() + } +} + +extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelegate { + func removeAttachment(_ cell: ComposeAttachmentTableViewCell) { + guard let indexPath = tableView.indexPath(for: cell) else { return } + attachments.remove(at: indexPath.row) + tableView.performBatchUpdates({ + tableView.deleteRows(at: [indexPath], with: .automatic) + }, completion: { (_) in + // when removing cells, we don't trigger the container height update until after the animation has completed + // otherwise, during the animation, the height is too short and the last row briefly disappears + self.updateHeightConstraint() + }) + } + + func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell) { + delegate?.composeRequiresAttachmentDescriptionsDidChange() + } +} diff --git a/Tusker/Screens/Compose/Attachments/CompositionAttachment.swift b/Tusker/Screens/Compose/Attachments/CompositionAttachment.swift new file mode 100644 index 00000000..477e09c3 --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/CompositionAttachment.swift @@ -0,0 +1,25 @@ +// +// CompositionAttachment.swift +// Tusker +// +// Created by Shadowfacts on 3/14/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import Foundation + +class CompositionAttachment: Codable { + let data: CompositionAttachmentData + var description: String + + init(data: CompositionAttachmentData, description: String = "") { + self.data = data + self.description = description + } +} + +extension CompositionAttachment: Equatable { + static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool { + return lhs.data == rhs.data + } +} diff --git a/Tusker/Screens/Compose/Attachments/CompositionAttachmentData.swift b/Tusker/Screens/Compose/Attachments/CompositionAttachmentData.swift new file mode 100644 index 00000000..588fc142 --- /dev/null +++ b/Tusker/Screens/Compose/Attachments/CompositionAttachmentData.swift @@ -0,0 +1,185 @@ +// +// CompositionAttachmentData.swift +// Tusker +// +// Created by Shadowfacts on 1/1/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos +import MobileCoreServices + +enum CompositionAttachmentData { + case asset(PHAsset) + case image(UIImage) + case video(URL) + + var type: AttachmentType { + switch self { + case let .asset(asset): + return asset.attachmentType! + case .image(_): + return .image + case .video(_): + return .video + } + } + + var isAsset: Bool { + switch self { + case .asset(_): + return true + default: + return false + } + } + + var canSaveToDraft: Bool { + switch self { + case .video(_): + return false + default: + return true + } + } + + func getData(completion: @escaping (Data, String) -> Void) { + switch self { + case let .image(image): + completion(image.pngData()!, "image/png") + case let .asset(asset): + if asset.mediaType == .image { + let options = PHImageRequestOptions() + options.version = .current + options.deliveryMode = .highQualityFormat + options.resizeMode = .none + options.isNetworkAccessAllowed = true + PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in + guard var data = data, let dataUTI = dataUTI else { fatalError() } + + let mimeType: String + if dataUTI == "public.heic" { + // neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG + let image = CIImage(data: data)! + let context = CIContext() + let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)! + data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])! + mimeType = "image/jpeg" + } else { + mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String + } + + completion(data, mimeType) + } + } else if asset.mediaType == .video { + let options = PHVideoRequestOptions() + options.deliveryMode = .automatic + options.isNetworkAccessAllowed = true + options.version = .current + PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in + guard let exportSession = exportSession else { fatalError("failed to create export session") } + CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion) + } + } else { + fatalError("assetType must be either image or video") + } + case let .video(url): + let asset = AVURLAsset(url: url) + guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else { + fatalError("failed to create export session") + } + CompositionAttachmentData.exportVideoData(session: session, completion: completion) + } + } + + private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) { + session.outputFileType = .mp4 + session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4") + session.exportAsynchronously { + guard session.status == .completed else { fatalError("video export failed: \(String(describing: session.error))") } + do { + let data = try Data(contentsOf: session.outputURL!) + completion(data, "video/mp4") + } catch { + fatalError("Unable to load video: \(error)") + } + } + } + + enum AttachmentType { + case image, video + } +} + +extension PHAsset { + var attachmentType: CompositionAttachmentData.AttachmentType? { + switch self.mediaType { + case .image: + return .image + case .video: + return .video + default: + return nil + } + } +} + +extension CompositionAttachmentData: Codable { + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .asset(asset): + try container.encode("asset", forKey: .type) + try container.encode(asset.localIdentifier, forKey: .assetIdentifier) + case let .image(image): + try container.encode("image", forKey: .type) + try container.encode(image.pngData()!, forKey: .imageData) + case .video(_): + throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded")) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(String.self, forKey: .type) { + case "asset": + let identifier = try container.decode(String.self, forKey: .assetIdentifier) + guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else { + throw DecodingError.dataCorruptedError(forKey: .assetIdentifier, in: container, debugDescription: "Could not fetch asset with local identifier") + } + self = .asset(asset) + case "image": + guard let image = UIImage(data: try container.decode(Data.self, forKey: .imageData)) else { + throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data") + } + self = .image(image) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'") + } + } + + enum CodingKeys: CodingKey { + case type + case imageData + /// The local identifier of the PHAsset for this attachment + case assetIdentifier + } +} + +extension CompositionAttachmentData: Equatable { + static func ==(lhs: CompositionAttachmentData, rhs: CompositionAttachmentData) -> Bool { + switch (lhs, rhs) { + case let (.asset(a), .asset(b)): + return a.localIdentifier == b.localIdentifier + case let (.image(a), .image(b)): + return a == b + case let (.video(a), .video(b)): + return a == b + default: + return false + } + } +} diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 6e3a05a7..4cad8a08 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -27,11 +27,6 @@ class ComposeViewController: UIViewController { visibilityChanged() } } - var selectedAttachments: [CompositionAttachment] = [] { - didSet { - updateAttachmentViews() - } - } var hasChanges = false var currentDraft: DraftsManager.Draft? @@ -67,11 +62,12 @@ class ComposeViewController: UIViewController { @IBOutlet weak var contentWarningContainerView: UIView! @IBOutlet weak var contentWarningTextField: UITextField! - @IBOutlet weak var attachmentsStackView: UIStackView! - @IBOutlet weak var addAttachmentButton: UIButton! - + @IBOutlet weak var composeAttachmentsContainerView: UIView! + @IBOutlet weak var postProgressView: SteppedProgressView! + var composeAttachmentsViewController: ComposeAttachmentsViewController! + init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, mastodonController: MastodonController) { self.mastodonController = mastodonController @@ -142,9 +138,15 @@ class ComposeViewController: UIViewController { // we have to set the font here, because the monospaced digit font is not available in IB charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular) updateCharactersRemaining() - updateAttachmentDescriptionsRequired() updatePlaceholder() + composeAttachmentsViewController = ComposeAttachmentsViewController(attachments: currentDraft?.attachments ?? [], mastodonController: mastodonController) + composeRequiresAttachmentDescriptionsDidChange() + composeAttachmentsViewController.delegate = self + composeAttachmentsViewController.tableView.isScrollEnabled = false + composeAttachmentsViewController.tableView.translatesAutoresizingMaskIntoConstraints = false + embedChild(composeAttachmentsViewController, in: composeAttachmentsContainerView) + NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField) } @@ -218,18 +220,6 @@ class ComposeViewController: UIViewController { NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - let imageName: String - if traitCollection.userInterfaceStyle == .dark { - imageName = "photo.fill" - } else { - imageName = "photo" - } - addAttachmentButton.setImage(UIImage(systemName: imageName), for: .normal) - } - func createFormattingButtons() -> [UIBarButtonItem] { guard Preferences.shared.statusContentType != .plain else { return [] @@ -271,19 +261,6 @@ class ComposeViewController: UIViewController { scrollView.scrollIndicatorInsets = scrollView.contentInset } - func updateAttachmentDescriptionsRequired() { - if Preferences.shared.requireAttachmentDescriptions { - for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews { - if mediaView.descriptionTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - compositionState.formUnion(.requiresAttachmentDescriptions) - return - } - } - - } - compositionState.subtract(.requiresAttachmentDescriptions) - } - func updateCharactersRemaining() { let count = CharacterCounter.count(text: statusTextView.text) let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0 @@ -312,31 +289,6 @@ class ComposeViewController: UIViewController { placeholderLabel.isHidden = !statusTextView.text.isEmpty } - func updateAddAttachmentButton() { - switch mastodonController.instance.instanceType { - case .pleroma: - addAttachmentButton.isEnabled = true - case .mastodon: - addAttachmentButton.isEnabled = selectedAttachments.count <= 4 && !selectedAttachments.contains(where: { $0.type == .video }) - } - } - - func updateAttachmentViews() { - for view in attachmentsStackView.arrangedSubviews { - if view is ComposeMediaView { - view.removeFromSuperview() - } - } - - for attachment in selectedAttachments { - let mediaView = ComposeMediaView.create() - mediaView.delegate = self - mediaView.update(attachment: attachment) - attachmentsStackView.insertArrangedSubview(mediaView, at: attachmentsStackView.arrangedSubviews.count - 1) - updateAddAttachmentButton() - } - } - func contentWarningStateChanged() { contentWarningContainerView.isHidden = !contentWarningEnabled if contentWarningEnabled { @@ -352,13 +304,7 @@ class ComposeViewController: UIViewController { } func saveDraft() { - var attachments = [DraftsManager.DraftAttachment]() - for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews - where mediaView.attachment.canSaveToDraft { - let attachment = mediaView.attachment! - let description = mediaView.descriptionTextView.text ?? "" - attachments.append(.init(attachment: attachment, description: description)) - } + let attachments = composeAttachmentsViewController.attachments let statusText = statusTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) let cw = contentWarningEnabled ? contentWarningTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) : nil let account = mastodonController.accountInfo! @@ -462,17 +408,7 @@ class ComposeViewController: UIViewController { draftsVC.delegate = self present(UINavigationController(rootViewController: draftsVC), animated: true) } - - @IBAction func addAttachmentPressed(_ sender: Any) { - // hide keyboard before showing asset picker, so it doesn't re-appear when asset picker is closed - contentWarningTextField.resignFirstResponder() - statusTextView.resignFirstResponder() - - let sheetContainer = AssetPickerSheetContainerViewController() - sheetContainer.assetPicker.assetPickerDelegate = self - present(sheetContainer, animated: true) - } - + @objc func postButtonPressed() { guard let text = statusTextView.text, !text.isEmpty else { return } @@ -492,48 +428,20 @@ class ComposeViewController: UIViewController { let sensitive = contentWarning != nil let visibility = self.visibility! - let group = DispatchGroup() - - var attachments: [Attachment?] = [] - for compAttachment in selectedAttachments { - let index = attachments.count - attachments.append(nil) - - let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView - let description = mediaView.descriptionTextView.text - - group.enter() - - compAttachment.getData { (data, mimeType) in - self.postProgressView.step() - - let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description) - self.mastodonController.run(request) { (response) in - guard case let .success(attachment, _) = response else { fatalError() } - - attachments[index] = attachment - - self.postProgressView.step() - - group.leave() - } - } - } - - postProgressView.steps = 2 + (attachments.count * 2) // 2 steps (request data, then upload) for each attachment + postProgressView.steps = 2 + (composeAttachmentsViewController.attachments.count * 2) // 2 steps (request data, then upload) for each attachment postProgressView.currentStep = 1 - group.notify(queue: .main) { - let attachments = attachments.compactMap { $0 } + composeAttachmentsViewController.uploadAll(stepProgress: postProgressView.step) { (success, uploadedAttachments) in + guard success else { return } let request = Client.createStatus(text: text, - contentType: Preferences.shared.statusContentType, - inReplyTo: self.inReplyToID, - media: attachments, - sensitive: sensitive, - spoilerText: contentWarning, - visibility: visibility, - language: nil) + contentType: Preferences.shared.statusContentType, + inReplyTo: self.inReplyToID, + media: uploadedAttachments, + sensitive: sensitive, + spoilerText: contentWarning, + visibility: visibility, + language: nil) self.mastodonController.run(request) { (response) in guard case let .success(status, _) = response else { fatalError() } self.postedStatus = status @@ -547,6 +455,7 @@ class ComposeViewController: UIViewController { self.postProgressView.step() self.dismiss(animated: true) + // todo: this doesn't work let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController) self.show(conversationVC, sender: self) @@ -586,36 +495,17 @@ extension ComposeViewController: UITextViewDelegate { } } -extension ComposeViewController: AssetPickerViewControllerDelegate { - func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachment.AttachmentType) -> Bool { - switch mastodonController.instance.instanceType { - case .pleroma: - return true - case .mastodon: - if (type == .video && selectedAttachments.count > 0) || - selectedAttachments.contains(where: { $0.type == .video }) || - assetPicker.currentCollectionSelectedAssets.contains(where: { $0.type == .video }) { - return false - } - return selectedAttachments.count + assetPicker.currentCollectionSelectedAssets.count < 4 - } - } - func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) { - selectedAttachments.append(contentsOf: attachments) - updateAttachmentDescriptionsRequired() - } -} - -extension ComposeViewController: ComposeMediaViewDelegate { - func didRemoveMedia(_ mediaView: ComposeMediaView) { - let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)! - selectedAttachments.remove(at: index) - updateAddAttachmentButton() - updateAttachmentDescriptionsRequired() +extension ComposeViewController: ComposeAttachmentsViewControllerDelegate { + func composeSelectedAttachmentsDidChange() { + currentDraft?.attachments = composeAttachmentsViewController.attachments } - func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) { - updateAttachmentDescriptionsRequired() + func composeRequiresAttachmentDescriptionsDidChange() { + if composeAttachmentsViewController.requiresAttachmentDescriptions { + compositionState.formUnion(.requiresAttachmentDescriptions) + } else { + compositionState.subtract(.requiresAttachmentDescriptions) + } } } @@ -657,26 +547,16 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { updatePlaceholder() updateCharactersRemaining() - selectedAttachments = draft.attachments.map { $0.attachment } - updateAttachmentViews() - - for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews { - let attachment = draft.attachments.first(where: { $0.attachment == mediaView.attachment })! - mediaView.descriptionTextView.text = attachment.description - - // call the delegate method manually, since setting the text property doesn't call it - mediaView.textViewDidChange(mediaView.descriptionTextView) - } - - updateAttachmentDescriptionsRequired() + composeAttachmentsViewController.setAttachments(draft.attachments) } func draftSelectionCompleted() { + // todo: I don't think this can actually happen any more? // check that all the assets from the draft have been added - if let currentDraft = currentDraft, selectedAttachments.count < currentDraft.attachments.count { + if let currentDraft = currentDraft, composeAttachmentsViewController.attachments.count < currentDraft.attachments.count { // some of the assets in the draft weren't loaded, so notify the user - let difference = currentDraft.attachments.count - selectedAttachments.count + let difference = currentDraft.attachments.count - composeAttachmentsViewController.attachments.count // todo: localize me let suffix = difference == 1 ? "" : "s" let verb = difference == 1 ? "was" : "were" diff --git a/Tusker/Screens/Compose/ComposeViewController.xib b/Tusker/Screens/Compose/ComposeViewController.xib index 3d052e77..e13d5290 100644 --- a/Tusker/Screens/Compose/ComposeViewController.xib +++ b/Tusker/Screens/Compose/ComposeViewController.xib @@ -1,17 +1,16 @@ - + - + - - + @@ -31,14 +30,14 @@ - + - + - - + + @@ -134,56 +133,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + @@ -222,7 +175,4 @@ - - - diff --git a/Tusker/Screens/Compose/CompositionAttachment.swift b/Tusker/Screens/Compose/CompositionAttachment.swift index 87a8fe35..42c758ab 100644 --- a/Tusker/Screens/Compose/CompositionAttachment.swift +++ b/Tusker/Screens/Compose/CompositionAttachment.swift @@ -10,7 +10,7 @@ import UIKit import Photos import MobileCoreServices -enum CompositionAttachment { +enum CompositionAttachmentData { case asset(PHAsset) case image(UIImage) case video(URL) @@ -79,7 +79,7 @@ enum CompositionAttachment { options.version = .current PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in guard let exportSession = exportSession else { fatalError("failed to create export session") } - CompositionAttachment.exportVideoData(session: exportSession, completion: completion) + CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion) } } else { fatalError("assetType must be either image or video") @@ -89,7 +89,7 @@ enum CompositionAttachment { guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else { fatalError("failed to create export session") } - CompositionAttachment.exportVideoData(session: session, completion: completion) + CompositionAttachmentData.exportVideoData(session: session, completion: completion) } } @@ -113,7 +113,7 @@ enum CompositionAttachment { } extension PHAsset { - var attachmentType: CompositionAttachment.AttachmentType? { + var attachmentType: CompositionAttachmentData.AttachmentType? { switch self.mediaType { case .image: return .image @@ -125,7 +125,7 @@ extension PHAsset { } } -extension CompositionAttachment: Codable { +extension CompositionAttachmentData: Codable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -169,13 +169,15 @@ extension CompositionAttachment: Codable { } } -extension CompositionAttachment: Equatable { - static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool { +extension CompositionAttachmentData: Equatable { + static func ==(lhs: CompositionAttachmentData, rhs: CompositionAttachmentData) -> Bool { switch (lhs, rhs) { case let (.asset(a), .asset(b)): return a.localIdentifier == b.localIdentifier case let (.image(a), .image(b)): return a == b + case let (.video(a), .video(b)): + return a == b default: return false } diff --git a/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift b/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift index b8414f0f..ae1dd6e4 100644 --- a/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift +++ b/Tusker/Screens/Compose/Drafts/DraftTableViewCell.swift @@ -34,7 +34,7 @@ class DraftTableViewCell: UITableViewCell { attachmentsStackView.addArrangedSubview(imageView) imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true - switch attachment.attachment { + switch attachment.data { case let .asset(asset): PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in imageView.image = image diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift index cd7ba31f..b1c5fd0e 100644 --- a/Tusker/Views/Compose Media/ComposeMediaView.swift +++ b/Tusker/Views/Compose Media/ComposeMediaView.swift @@ -23,7 +23,7 @@ class ComposeMediaView: UIView { @IBOutlet weak var descriptionTextView: UITextView! @IBOutlet weak var placeholderLabel: UILabel! - var attachment: CompositionAttachment! + var attachment: CompositionAttachmentData! static func create() -> ComposeMediaView { return UINib(nibName: "ComposeMediaView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeMediaView @@ -38,7 +38,7 @@ class ComposeMediaView: UIView { descriptionTextView.delegate = self } - func update(attachment: CompositionAttachment) { + func update(attachment: CompositionAttachmentData) { self.attachment = attachment switch attachment {