diff --git a/Localization/app.json b/Localization/app.json index 10c672a73..c0a305d96 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -219,6 +219,12 @@ }, "content_warning": { "placeholder": "Write an accurate warning here..." + }, + "visibility": { + "public": "Public", + "unlisted": "Unlisted", + "private": "Followers only", + "direct": "Only people I mention" } } } diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 4a0e90d76..85af26678 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -100,6 +100,8 @@ extension ComposeStatusSection { } .store(in: &cell.disposeBag) ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag) + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) + return cell case .attachment(let attachmentService): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 8e7da586d..a875994ea 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -198,6 +198,16 @@ internal enum L10n { /// New Reply internal static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply") } + internal enum Visibility { + /// Only people I mention + internal static let direct = L10n.tr("Localizable", "Scene.Compose.Visibility.Direct") + /// Followers only + internal static let `private` = L10n.tr("Localizable", "Scene.Compose.Visibility.Private") + /// Public + internal static let `public` = L10n.tr("Localizable", "Scene.Compose.Visibility.Public") + /// Unlisted + internal static let unlisted = L10n.tr("Localizable", "Scene.Compose.Visibility.Unlisted") + } } internal enum ConfirmEmail { /// We just sent an email to %@,\ntap the link to confirm your account. diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 860bf2db5..d2ebb4071 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -60,6 +60,10 @@ uploaded to Mastodon."; "Scene.Compose.Poll.ThreeDays" = "3 Days"; "Scene.Compose.Title.NewPost" = "New Post"; "Scene.Compose.Title.NewReply" = "New Reply"; +"Scene.Compose.Visibility.Direct" = "Only people I mention"; +"Scene.Compose.Visibility.Private" = "Followers only"; +"Scene.Compose.Visibility.Public" = "Public"; +"Scene.Compose.Visibility.Unlisted" = "Unlisted"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 7e1dc07b3..1de686201 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -270,6 +270,14 @@ extension ComposeViewController { self.resetImagePicker() } .store(in: &disposeBag) + + viewModel.selectedStatusVisibility + .receive(on: DispatchQueue.main) + .sink { [weak self] type in + guard let self = self else { return } + self.composeToolbarView.visibilityButton.setImage(type.image, for: .normal) + } + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { @@ -589,8 +597,8 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate { // MARK: - ComposeToolbarViewDelegate extension ComposeViewController: ComposeToolbarViewDelegate { - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) { - switch mediaSelectionType { + func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { + switch type { case .photoLibrary: present(imagePicker, animated: true, completion: nil) case .camera: @@ -626,7 +634,8 @@ extension ComposeViewController: ComposeToolbarViewDelegate { viewModel.isContentWarningComposing.value.toggle() } - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton) { + func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { + viewModel.selectedStatusVisibility.value = type } } diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift index 222ac938a..d5047cc9f 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift @@ -69,6 +69,7 @@ extension ComposeViewModel.PublishState { } return text }() + let visibility = viewModel.selectedStatusVisibility.value.visibility let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { var subscriptions: [AnyPublisher, Error>] = [] @@ -102,7 +103,8 @@ extension ComposeViewModel.PublishState { pollOptions: pollOptions, pollExpiresIn: pollExpiresIn, sensitive: sensitive, - spoilerText: spoilerText + spoilerText: spoilerText, + visibility: visibility ) return viewModel.context.apiService.publishStatus( domain: domain, diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index c2e357d01..07570d796 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import GameplayKit +import MastodonSDK final class ComposeViewModel { @@ -22,6 +23,7 @@ final class ComposeViewModel { let isPollComposing = CurrentValueSubject(false) let isCustomEmojiComposing = CurrentValueSubject(false) let isContentWarningComposing = CurrentValueSubject(false) + let selectedStatusVisibility = CurrentValueSubject(.public) let activeAuthentication: CurrentValueSubject let activeAuthenticationBox: CurrentValueSubject diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index b109faf3e..8ba879c02 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -7,13 +7,14 @@ import os.log import UIKit +import MastodonSDK protocol ComposeToolbarViewDelegate: class { - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) + func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton) func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton) + func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) } final class ComposeToolbarView: UIView { @@ -106,7 +107,8 @@ extension ComposeToolbarView { pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside) emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside) contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside) - visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.visibilityButtonDidPressed(_:)), for: .touchUpInside) + visibilityButton.menu = createVisibilityContextMenu() + visibilityButton.showsMenuAsPrimaryAction = true } } @@ -116,6 +118,40 @@ extension ComposeToolbarView { case photoLibrary case browse } + + enum VisibilitySelectionType: String, CaseIterable { + case `public` + case unlisted + case `private` + case direct + + var title: String { + switch self { + case .public: return L10n.Scene.Compose.Visibility.public + case .unlisted: return L10n.Scene.Compose.Visibility.unlisted + case .private: return L10n.Scene.Compose.Visibility.private + case .direct: return L10n.Scene.Compose.Visibility.direct + } + } + + var image: UIImage { + switch self { + case .public: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))! + case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))! + case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))! + case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))! + } + } + + var visibility: Mastodon.Entity.Status.Visibility { + switch self { + case .public: return .public + case .unlisted: return .unlisted + case .private: return .private + case .direct: return .direct + } + } + } } extension ComposeToolbarView { @@ -154,9 +190,19 @@ extension ComposeToolbarView { return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) } + private func createVisibilityContextMenu() -> UIMenu { + let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in + UIAction(title: type.title, image: type.image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in + guard let self = self else { return } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue) + self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type) + } + } + return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) + } + } - extension ComposeToolbarView { @objc private func pollButtonDidPressed(_ sender: UIButton) { @@ -174,11 +220,6 @@ extension ComposeToolbarView { delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender) } - @objc private func visibilityButtonDidPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.composeToolbarView(self, visibilityButtonDidPressed: sender) - } - } #if canImport(SwiftUI) && DEBUG diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift index d1fb95f40..da54c9344 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift @@ -100,6 +100,7 @@ extension Mastodon.API.Statuses { public let pollExpiresIn: Int? public let sensitive: Bool? public let spoilerText: String? + public let visibility: Mastodon.Entity.Status.Visibility? public init( status: String?, @@ -107,7 +108,8 @@ extension Mastodon.API.Statuses { pollOptions: [String]?, pollExpiresIn: Int?, sensitive: Bool?, - spoilerText: String? + spoilerText: String?, + visibility: Mastodon.Entity.Status.Visibility? ) { self.status = status self.mediaIDs = mediaIDs @@ -115,6 +117,7 @@ extension Mastodon.API.Statuses { self.pollExpiresIn = pollExpiresIn self.sensitive = sensitive self.spoilerText = spoilerText + self.visibility = visibility } @@ -135,6 +138,7 @@ extension Mastodon.API.Statuses { pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) } sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) } spoilerText.flatMap { data.append(Data.multipart(key: "spoiler_text", value: $0)) } + visibility.flatMap { data.append(Data.multipart(key: "visibility", value: $0.rawValue)) } data.append(Data.multipartEnd()) return data