diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 900721606b..8402fc82f2 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; }; D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; }; D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; }; + D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483123D2A6A3008A63EF /* CompositionState.swift */; }; D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; }; D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; }; D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; }; @@ -344,6 +345,7 @@ D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = ""; }; D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = ""; }; D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = ""; }; + D620483123D2A6A3008A63EF /* CompositionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionState.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 = ""; }; D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = ""; }; diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index a0e50b4664..75fdaa9706 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -45,6 +45,7 @@ class Preferences: Codable, ObservableObject { self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility) self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts) self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) + self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions) self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia) self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps) self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) @@ -68,6 +69,7 @@ class Preferences: Codable, ObservableObject { try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility) try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts) try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) + try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions) try container.encode(blurAllMedia, forKey: .blurAllMedia) try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(useInAppSafari, forKey: .useInAppSafari) @@ -90,6 +92,7 @@ class Preferences: Codable, ObservableObject { @Published var defaultPostVisibility = Status.Visibility.public @Published var automaticallySaveDrafts = true @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs + @Published var requireAttachmentDescriptions = false @Published var blurAllMedia = false @Published var openLinksInApps = true @Published var useInAppSafari = true @@ -112,6 +115,7 @@ class Preferences: Codable, ObservableObject { case defaultPostVisibility case automaticallySaveDrafts case contentWarningCopyMode + case requireAttachmentDescriptions case blurAllMedia case openLinksInApps case useInAppSafari diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 4caace129e..165fa0bbd3 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -37,6 +37,12 @@ class ComposeViewController: UIViewController { weak var xcbSession: XCBSession? var postedStatus: Status? + var compositionState: CompositionState = .valid { + didSet { + postBarButtonItem.isEnabled = compositionState.isValid + } + } + weak var postBarButtonItem: UIBarButtonItem! var visibilityBarButtonItem: UIBarButtonItem! var contentWarningBarButtonItem: UIBarButtonItem! @@ -131,6 +137,7 @@ 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() NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField) @@ -266,17 +273,29 @@ 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() { - // TODO: include CW char count let count = CharacterCounter.count(text: statusTextView.text) let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0 let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount if remaining < 0 { charactersRemainingLabel.textColor = .red - postBarButtonItem.isEnabled = false + compositionState.formUnion(.tooManyCharacters) } else { charactersRemainingLabel.textColor = .darkGray - postBarButtonItem.isEnabled = true + compositionState.subtract(.tooManyCharacters) } charactersRemainingLabel.text = String(remaining) charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining) @@ -454,7 +473,7 @@ class ComposeViewController: UIViewController { saveDraft() // disable post button while sending post request - postBarButtonItem.isEnabled = false + compositionState.formUnion(.currentlyPosting) let contentWarning: String? if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty { @@ -575,6 +594,7 @@ extension ComposeViewController: AssetPickerViewControllerDelegate { } func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) { selectedAttachments.append(contentsOf: attachments) + updateAttachmentDescriptionsRequired() } } @@ -583,6 +603,11 @@ extension ComposeViewController: ComposeMediaViewDelegate { let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)! selectedAttachments.remove(at: index) updateAddAttachmentButton() + updateAttachmentDescriptionsRequired() + } + + func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) { + updateAttachmentDescriptionsRequired() } } @@ -620,7 +645,7 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { updatePlaceholder() updateCharactersRemaining() - + selectedAttachments = draft.attachments.map { $0.attachment } updateAttachmentViews() @@ -631,6 +656,8 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { // call the delegate method manually, since setting the text property doesn't call it mediaView.textViewDidChange(mediaView.descriptionTextView) } + + updateAttachmentDescriptionsRequired() } func draftSelectionCompleted() { diff --git a/Tusker/Screens/Compose/CompositionState.swift b/Tusker/Screens/Compose/CompositionState.swift new file mode 100644 index 0000000000..950ff821bd --- /dev/null +++ b/Tusker/Screens/Compose/CompositionState.swift @@ -0,0 +1,23 @@ +// +// CompositionState.swift +// Tusker +// +// Created by Shadowfacts on 1/17/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import Foundation + +struct CompositionState: OptionSet { + let rawValue: Int + + static let currentlyPosting = CompositionState(rawValue: 1 << 0) + static let tooManyCharacters = CompositionState(rawValue: 1 << 1) + static let requiresAttachmentDescriptions = CompositionState(rawValue: 1 << 2) + + static let valid: CompositionState = [] + + var isValid: Bool { + isEmpty + } +} diff --git a/Tusker/Screens/Preferences/BehaviorPrefsView.swift b/Tusker/Screens/Preferences/BehaviorPrefsView.swift index f7074cd8df..bb07a3226b 100644 --- a/Tusker/Screens/Preferences/BehaviorPrefsView.swift +++ b/Tusker/Screens/Preferences/BehaviorPrefsView.swift @@ -40,6 +40,9 @@ struct BehaviorPrefsView: View { Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe) Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy) } + Toggle(isOn: $preferences.requireAttachmentDescriptions) { + Text("Require Attachment Descriptions") + } } } diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift index d11a418b2a..54c1efd7b4 100644 --- a/Tusker/Views/Compose Media/ComposeMediaView.swift +++ b/Tusker/Views/Compose Media/ComposeMediaView.swift @@ -12,6 +12,7 @@ import AVFoundation protocol ComposeMediaViewDelegate { func didRemoveMedia(_ mediaView: ComposeMediaView) + func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) } class ComposeMediaView: UIView { @@ -69,5 +70,6 @@ class ComposeMediaView: UIView { extension ComposeMediaView: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { placeholderLabel.isHidden = !descriptionTextView.text.isEmpty + delegate?.descriptionTextViewDidChange(self) } }