feat: add status visibility selector for status compose scene
This commit is contained in:
parent
610ee36835
commit
00e7450bcc
|
@ -219,6 +219,12 @@
|
||||||
},
|
},
|
||||||
"content_warning": {
|
"content_warning": {
|
||||||
"placeholder": "Write an accurate warning here..."
|
"placeholder": "Write an accurate warning here..."
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"public": "Public",
|
||||||
|
"unlisted": "Unlisted",
|
||||||
|
"private": "Followers only",
|
||||||
|
"direct": "Only people I mention"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,8 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &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
|
return cell
|
||||||
case .attachment(let attachmentService):
|
case .attachment(let attachmentService):
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
||||||
|
|
|
@ -198,6 +198,16 @@ internal enum L10n {
|
||||||
/// New Reply
|
/// New Reply
|
||||||
internal static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply")
|
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 {
|
internal enum ConfirmEmail {
|
||||||
/// We just sent an email to %@,\ntap the link to confirm your account.
|
/// We just sent an email to %@,\ntap the link to confirm your account.
|
||||||
|
|
|
@ -60,6 +60,10 @@ uploaded to Mastodon.";
|
||||||
"Scene.Compose.Poll.ThreeDays" = "3 Days";
|
"Scene.Compose.Poll.ThreeDays" = "3 Days";
|
||||||
"Scene.Compose.Title.NewPost" = "New Post";
|
"Scene.Compose.Title.NewPost" = "New Post";
|
||||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
"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.DontReceiveEmail" = "I never got an email";
|
||||||
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
|
"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.";
|
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";
|
||||||
|
|
|
@ -270,6 +270,14 @@ extension ComposeViewController {
|
||||||
self.resetImagePicker()
|
self.resetImagePicker()
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -589,8 +597,8 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
// MARK: - ComposeToolbarViewDelegate
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) {
|
||||||
switch mediaSelectionType {
|
switch type {
|
||||||
case .photoLibrary:
|
case .photoLibrary:
|
||||||
present(imagePicker, animated: true, completion: nil)
|
present(imagePicker, animated: true, completion: nil)
|
||||||
case .camera:
|
case .camera:
|
||||||
|
@ -626,7 +634,8 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
viewModel.isContentWarningComposing.value.toggle()
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ extension ComposeViewModel.PublishState {
|
||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
}()
|
}()
|
||||||
|
let visibility = viewModel.selectedStatusVisibility.value.visibility
|
||||||
|
|
||||||
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||||
var subscriptions: [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,
|
pollOptions: pollOptions,
|
||||||
pollExpiresIn: pollExpiresIn,
|
pollExpiresIn: pollExpiresIn,
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoilerText: spoilerText
|
spoilerText: spoilerText,
|
||||||
|
visibility: visibility
|
||||||
)
|
)
|
||||||
return viewModel.context.apiService.publishStatus(
|
return viewModel.context.apiService.publishStatus(
|
||||||
domain: domain,
|
domain: domain,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
final class ComposeViewModel {
|
final class ComposeViewModel {
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ final class ComposeViewModel {
|
||||||
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
||||||
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
|
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
|
||||||
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let selectedStatusVisibility = CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>(.public)
|
||||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,14 @@
|
||||||
|
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
protocol ComposeToolbarViewDelegate: class {
|
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, pollButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed 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 {
|
final class ComposeToolbarView: UIView {
|
||||||
|
@ -106,7 +107,8 @@ extension ComposeToolbarView {
|
||||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
|
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), 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 photoLibrary
|
||||||
case browse
|
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 {
|
extension ComposeToolbarView {
|
||||||
|
@ -154,8 +190,18 @@ extension ComposeToolbarView {
|
||||||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
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 {
|
extension ComposeToolbarView {
|
||||||
|
|
||||||
|
@ -174,11 +220,6 @@ extension ComposeToolbarView {
|
||||||
delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender)
|
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
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
|
|
@ -100,6 +100,7 @@ extension Mastodon.API.Statuses {
|
||||||
public let pollExpiresIn: Int?
|
public let pollExpiresIn: Int?
|
||||||
public let sensitive: Bool?
|
public let sensitive: Bool?
|
||||||
public let spoilerText: String?
|
public let spoilerText: String?
|
||||||
|
public let visibility: Mastodon.Entity.Status.Visibility?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
status: String?,
|
status: String?,
|
||||||
|
@ -107,7 +108,8 @@ extension Mastodon.API.Statuses {
|
||||||
pollOptions: [String]?,
|
pollOptions: [String]?,
|
||||||
pollExpiresIn: Int?,
|
pollExpiresIn: Int?,
|
||||||
sensitive: Bool?,
|
sensitive: Bool?,
|
||||||
spoilerText: String?
|
spoilerText: String?,
|
||||||
|
visibility: Mastodon.Entity.Status.Visibility?
|
||||||
) {
|
) {
|
||||||
self.status = status
|
self.status = status
|
||||||
self.mediaIDs = mediaIDs
|
self.mediaIDs = mediaIDs
|
||||||
|
@ -115,6 +117,7 @@ extension Mastodon.API.Statuses {
|
||||||
self.pollExpiresIn = pollExpiresIn
|
self.pollExpiresIn = pollExpiresIn
|
||||||
self.sensitive = sensitive
|
self.sensitive = sensitive
|
||||||
self.spoilerText = spoilerText
|
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)) }
|
pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
|
||||||
sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) }
|
sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) }
|
||||||
spoilerText.flatMap { data.append(Data.multipart(key: "spoiler_text", 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())
|
data.append(Data.multipartEnd())
|
||||||
return data
|
return data
|
||||||
|
|
Loading…
Reference in New Issue