feat: add status visibility selector for status compose scene

This commit is contained in:
CMK 2021-03-25 19:34:30 +08:00
parent 610ee36835
commit 00e7450bcc
9 changed files with 94 additions and 14 deletions

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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 havent.";

View File

@ -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
}
}

View File

@ -69,6 +69,7 @@ extension ComposeViewModel.PublishState {
}
return text
}()
let visibility = viewModel.selectedStatusVisibility.value.visibility
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, 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,

View File

@ -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<Bool, Never>(false)
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
let selectedStatusVisibility = CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>(.public)
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>

View File

@ -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

View File

@ -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