diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 4c3507f..e2830b9 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -45,6 +45,7 @@ "cancel" = "Cancel"; "compose.attachment.uploading" = "Uploading"; "compose.prompt" = "What's on your mind?"; +"compose.mark-media-sensitive" = "Mark media as sensitive"; "error" = "Error"; "favorites" = "Favorites"; "registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue"; diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/StatusEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/StatusEndpoint.swift index 30746fa..da7f741 100644 --- a/MastodonAPI/Sources/MastodonAPI/Endpoints/StatusEndpoint.swift +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/StatusEndpoint.swift @@ -22,18 +22,21 @@ public extension StatusEndpoint { public let spoilerText: String public let mediaIds: [Attachment.Id] public let visibility: Status.Visibility + public let sensitive: Bool public init( inReplyToId: Status.Id?, text: String, spoilerText: String, mediaIds: [Attachment.Id], - visibility: Status.Visibility) { + visibility: Status.Visibility, + sensitive: Bool) { self.inReplyToId = inReplyToId self.text = text self.spoilerText = spoilerText self.mediaIds = mediaIds self.visibility = visibility + self.sensitive = sensitive } } } @@ -57,6 +60,10 @@ extension StatusEndpoint.Components { params["in_reply_to_id"] = inReplyToId params["visibility"] = visibility.rawValue + if sensitive { + params["sensitive"] = true + } + return params } } diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index ea81787..ffe10c9 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -44,6 +44,8 @@ D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */; }; D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936F325AA66A600754FDF /* UIView+Extensions.swift */; }; D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936F325AA66A600754FDF /* UIView+Extensions.swift */; }; + D05936FF25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */; }; + D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */; }; D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; }; D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; }; D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; }; @@ -193,6 +195,7 @@ D05936DD25A937EC00754FDF /* EditThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditThumbnailView.swift; sourceTree = ""; }; D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAttachmentView.swift; sourceTree = ""; }; D05936F325AA66A600754FDF /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; + D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAttachmentsSensitiveView.swift; sourceTree = ""; }; D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = ""; }; D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = ""; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -464,6 +467,7 @@ D0B8510B25259E56004E0744 /* LoadMoreCell.swift */, D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */, D0E569DA2529319100FA1D72 /* LoadMoreView.swift */, + D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */, D03B1B29253818F3008F964B /* MediaPreferencesView.swift */, D0FCC10F259C4F20000B67DF /* NewStatusView.swift */, D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */, @@ -797,6 +801,7 @@ D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */, D0625E59250F092900502611 /* StatusListCell.swift in Sources */, D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */, + D05936FF25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */, D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */, D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */, D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */, @@ -872,6 +877,7 @@ D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */, D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */, D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */, + D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */, D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */, D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */, D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */, diff --git a/ViewModels/Sources/ViewModels/CompositionViewModel.swift b/ViewModels/Sources/ViewModels/CompositionViewModel.swift index 75650c7..d8fb7d4 100644 --- a/ViewModels/Sources/ViewModels/CompositionViewModel.swift +++ b/ViewModels/Sources/ViewModels/CompositionViewModel.swift @@ -11,6 +11,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab @Published public var text = "" @Published public var contentWarning = "" @Published public var displayContentWarning = false + @Published public var sensitive = false @Published public private(set) var attachmentViewModels = [AttachmentViewModel]() @Published public private(set) var attachmentUpload: AttachmentUpload? @Published public private(set) var isPostable = false @@ -45,6 +46,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab .combineLatest($displayContentWarning, $contentWarning) .map { Self.maxCharacters - ($0 + ($1 ? $2.count : 0)) } .assign(to: &$remainingCharacters) + $displayContentWarning.filter { $0 }.assign(to: &$sensitive) } public func attachmentSelected(viewModel: AttachmentViewModel) { @@ -72,7 +74,8 @@ public extension CompositionViewModel { text: text, spoilerText: displayContentWarning ? contentWarning : "", mediaIds: attachmentViewModels.map(\.attachment.id), - visibility: visibility) + visibility: visibility, + sensitive: sensitive) } func cancelUpload() { diff --git a/Views/CompositionView.swift b/Views/CompositionView.swift index a4ada95..f211e55 100644 --- a/Views/CompositionView.swift +++ b/Views/CompositionView.swift @@ -12,6 +12,7 @@ final class CompositionView: UIView { let textViewPlaceholder = UILabel() let attachmentsView = AttachmentsView() let attachmentUploadView: AttachmentUploadView + let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView private let viewModel: CompositionViewModel private let parentViewModel: NewStatusViewModel @@ -22,6 +23,7 @@ final class CompositionView: UIView { self.parentViewModel = parentViewModel attachmentUploadView = AttachmentUploadView(viewModel: viewModel) + markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel) super.init(frame: .zero) @@ -99,6 +101,7 @@ private extension CompositionView { stackView.addArrangedSubview(attachmentsView) stackView.addArrangedSubview(attachmentUploadView) + stackView.addArrangedSubview(markAttachmentsSensitiveView) textView.text = viewModel.text spoilerTextField.text = viewModel.contentWarning @@ -135,6 +138,7 @@ private extension CompositionView { .sink { [weak self] in self?.attachmentsView.viewModel = self?.viewModel self?.attachmentsView.isHidden = $0.isEmpty + self?.markAttachmentsSensitiveView.isHidden = $0.isEmpty } .store(in: &cancellables) diff --git a/Views/MarkAttachmentsSensitiveView.swift b/Views/MarkAttachmentsSensitiveView.swift new file mode 100644 index 0000000..af21da6 --- /dev/null +++ b/Views/MarkAttachmentsSensitiveView.swift @@ -0,0 +1,64 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Combine +import UIKit +import ViewModels + +final class MarkAttachmentsSensitiveView: UIView { + private let label = UILabel() + private let sensitiveSwitch = UISwitch() + private let viewModel: CompositionViewModel + private var cancellables = Set() + + init(viewModel: CompositionViewModel) { + self.viewModel = viewModel + + super.init(frame: .zero) + + initialSetup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension MarkAttachmentsSensitiveView { + func initialSetup() { + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + label.adjustsFontForContentSizeCategory = true + label.font = .preferredFont(forTextStyle: .callout) + label.textColor = .secondaryLabel + label.text = NSLocalizedString("compose.mark-media-sensitive", comment: "") + label.textAlignment = .right + + addSubview(sensitiveSwitch) + sensitiveSwitch.translatesAutoresizingMaskIntoConstraints = false + sensitiveSwitch.addAction( + UIAction { [weak self] _ in + guard let self = self else { return } + + self.viewModel.sensitive = self.sensitiveSwitch.isOn + }, + for: .valueChanged) + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), + sensitiveSwitch.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: .defaultSpacing), + sensitiveSwitch.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + sensitiveSwitch.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + sensitiveSwitch.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) + ]) + + viewModel.$sensitive + .sink { [weak self] in self?.sensitiveSwitch.setOn($0, animated: true) } + .store(in: &cancellables) + viewModel.$displayContentWarning + .sink { [weak self] in self?.sensitiveSwitch.isEnabled = !$0 } + .store(in: &cancellables) + } +}