From 5c5677507b58aa4aa7466274db062d821e65609e Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 10 Jan 2021 16:06:20 -0800 Subject: [PATCH] Post polls --- Localizations/Localizable.strings | 6 +- .../Endpoints/StatusEndpoint.swift | 21 ++- Metatext.xcodeproj/project.pbxproj | 12 ++ .../EditAttachmentViewController.swift | 1 - .../ViewModels/CompositionViewModel.swift | 51 +++++- Views/CompositionInputAccessoryView.swift | 7 +- Views/CompositionPollOptionView.swift | 87 +++++++++++ Views/CompositionPollView.swift | 146 ++++++++++++++++++ Views/CompositionView.swift | 23 ++- 9 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 Views/CompositionPollOptionView.swift create mode 100644 Views/CompositionPollView.swift diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index a01a10c..5a1a0a1 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -44,11 +44,13 @@ "camera-access.open-system-settings" = "Open system settings"; "cancel" = "Cancel"; "compose.attachment.uploading" = "Uploading"; -"compose.prompt" = "What's on your mind?"; +"compose.browse" = "Browse"; "compose.mark-media-sensitive" = "Mark media as sensitive"; "compose.photo-library" = "Photo Library"; +"compose.poll.add-choice" = "Add a choice"; +"compose.poll.allow-multiple-choices" = "Allow multiple choices"; +"compose.prompt" = "What's on your mind?"; "compose.take-photo-or-video" = "Take Photo or Video"; -"compose.browse" = "Browse"; "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 da7f741..04b3c67 100644 --- a/MastodonAPI/Sources/MastodonAPI/Endpoints/StatusEndpoint.swift +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/StatusEndpoint.swift @@ -23,6 +23,9 @@ public extension StatusEndpoint { public let mediaIds: [Attachment.Id] public let visibility: Status.Visibility public let sensitive: Bool + public let pollOptions: [String] + public let pollExpiresIn: Int + public let pollMultipleChoice: Bool public init( inReplyToId: Status.Id?, @@ -30,13 +33,19 @@ public extension StatusEndpoint { spoilerText: String, mediaIds: [Attachment.Id], visibility: Status.Visibility, - sensitive: Bool) { + sensitive: Bool, + pollOptions: [String], + pollExpiresIn: Int, + pollMultipleChoice: Bool) { self.inReplyToId = inReplyToId self.text = text self.spoilerText = spoilerText self.mediaIds = mediaIds self.visibility = visibility self.sensitive = sensitive + self.pollOptions = pollOptions + self.pollExpiresIn = pollExpiresIn + self.pollMultipleChoice = pollMultipleChoice } } } @@ -64,6 +73,16 @@ extension StatusEndpoint.Components { params["sensitive"] = true } + if !pollOptions.isEmpty { + var poll = [String: Any]() + + poll["options"] = pollOptions + poll["expires_in"] = pollExpiresIn + poll["multiple"] = pollMultipleChoice + + params["poll"] = poll + } + return params } } diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index ffe10c9..d3c3f74 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -46,6 +46,10 @@ 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 */; }; + D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373225AAEA7000754FDF /* CompositionPollView.swift */; }; + D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373225AAEA7000754FDF /* CompositionPollView.swift */; }; + D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; }; + D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.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 */; }; @@ -196,6 +200,8 @@ 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 = ""; }; + D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = ""; }; + D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.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; }; @@ -450,6 +456,8 @@ D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */, D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */, D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */, + D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */, + D059373225AAEA7000754FDF /* CompositionPollView.swift */, D08E52ED257D757100FA2C5F /* CompositionView.swift */, D007023D25562A2800F38136 /* ConversationAvatarsView.swift */, D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */, @@ -809,6 +817,7 @@ D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */, D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */, D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */, + D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */, D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */, D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */, D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */, @@ -824,6 +833,7 @@ D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */, D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */, D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */, + D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */, D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */, D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */, D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */, @@ -870,6 +880,7 @@ files = ( D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */, D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */, + D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */, D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */, D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */, D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */, @@ -881,6 +892,7 @@ D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */, D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */, D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */, + D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */, D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */, D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */, D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */, diff --git a/View Controllers/EditAttachmentViewController.swift b/View Controllers/EditAttachmentViewController.swift index d6fd3f4..de779f0 100644 --- a/View Controllers/EditAttachmentViewController.swift +++ b/View Controllers/EditAttachmentViewController.swift @@ -72,7 +72,6 @@ final class EditAttachmentViewController: UIViewController { stackView.addArrangedSubview(remainingCharactersLabel) remainingCharactersLabel.adjustsFontForContentSizeCategory = true remainingCharactersLabel.font = .preferredFont(forTextStyle: .subheadline) - remainingCharactersLabel.text = "1500" NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), diff --git a/ViewModels/Sources/ViewModels/CompositionViewModel.swift b/ViewModels/Sources/ViewModels/CompositionViewModel.swift index d8fb7d4..ab2f939 100644 --- a/ViewModels/Sources/ViewModels/CompositionViewModel.swift +++ b/ViewModels/Sources/ViewModels/CompositionViewModel.swift @@ -12,6 +12,11 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab @Published public var contentWarning = "" @Published public var displayContentWarning = false @Published public var sensitive = false + @Published public var displayPoll = false + @Published public var pollMultipleChoice = false + @Published public var pollHideTotals = false + @Published public var pollExpiresIn = PollExpiry.oneDay + @Published public private(set) var pollOptions = [PollOption(text: ""), PollOption(text: "")] @Published public private(set) var attachmentViewModels = [AttachmentViewModel]() @Published public private(set) var attachmentUpload: AttachmentUpload? @Published public private(set) var isPostable = false @@ -34,8 +39,8 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab } .assign(to: &$isPostable) $attachmentViewModels - .combineLatest($attachmentUpload) - .map { $0.count < Self.maxAttachmentCount && $1 == nil } + .combineLatest($attachmentUpload, $displayPoll) + .map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 } .assign(to: &$canAddAttachment) $attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment) $text.map { @@ -60,12 +65,35 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab public extension CompositionViewModel { static let maxCharacters = 500 + static let minPollOptionCount = 2 + static let maxPollOptionCount = 4 enum Event { case editAttachment(AttachmentViewModel, CompositionViewModel) case updateAttachment(AnyPublisher) } + enum PollExpiry: Int, CaseIterable { + case fiveMinutes = 300 + case thirtyMinutes = 1800 + case oneHour = 3600 + case sixHours = 21600 + case oneDay = 86400 + case threeDays = 259200 + case sevenDays = 604800 + } + + class PollOption: ObservableObject { + public let id = Id() + @Published public var text: String + @Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters + + public init(text: String) { + self.text = text + $text.map { Self.maxCharacters - $0.count }.assign(to: &$remainingCharacters) + } + } + typealias Id = UUID func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents { @@ -75,7 +103,18 @@ public extension CompositionViewModel { spoilerText: displayContentWarning ? contentWarning : "", mediaIds: attachmentViewModels.map(\.attachment.id), visibility: visibility, - sensitive: sensitive) + sensitive: sensitive, + pollOptions: displayPoll ? pollOptions.map(\.text) : [], + pollExpiresIn: pollExpiresIn.rawValue, + pollMultipleChoice: pollMultipleChoice) + } + + func addPollOption() { + pollOptions.append(PollOption(text: "")) + } + + func remove(pollOption: PollOption) { + pollOptions.removeAll { $0 === pollOption } } func cancelUpload() { @@ -100,6 +139,12 @@ public extension CompositionViewModel { } } +public extension CompositionViewModel.PollOption { + static let maxCharacters = 25 + + typealias Id = UUID +} + extension CompositionViewModel { func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) { attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider) diff --git a/Views/CompositionInputAccessoryView.swift b/Views/CompositionInputAccessoryView.swift index 89cce80..313248f 100644 --- a/Views/CompositionInputAccessoryView.swift +++ b/Views/CompositionInputAccessoryView.swift @@ -83,7 +83,7 @@ private extension CompositionInputAccessoryView { attachmentButton.showsMenuAsPrimaryAction = true attachmentButton.menu = UIMenu(children: attachmentActions) - let pollButton = UIButton() + let pollButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.viewModel.displayPoll.toggle() }) stackView.addArrangedSubview(pollButton) pollButton.setImage( @@ -134,6 +134,11 @@ private extension CompositionInputAccessoryView { .sink { attachmentButton.isEnabled = $0 } .store(in: &cancellables) + viewModel.$attachmentViewModels + .combineLatest(viewModel.$attachmentUpload) + .sink { pollButton.isEnabled = $0.isEmpty && $1 == nil } + .store(in: &cancellables) + viewModel.$remainingCharacters.sink { charactersLabel.text = String($0) charactersLabel.textColor = $0 < 0 ? .systemRed : .label diff --git a/Views/CompositionPollOptionView.swift b/Views/CompositionPollOptionView.swift new file mode 100644 index 0000000..3f797b3 --- /dev/null +++ b/Views/CompositionPollOptionView.swift @@ -0,0 +1,87 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Combine +import UIKit +import ViewModels + +final class CompositionPollOptionView: UIView { + let option: CompositionViewModel.PollOption + let removeButton = UIButton(type: .close) + private let viewModel: CompositionViewModel + private let compositionInputAccessoryView: CompositionInputAccessoryView + private var cancellables = Set() + + init(viewModel: CompositionViewModel, + option: CompositionViewModel.PollOption, + inputAccessoryView: CompositionInputAccessoryView) { + self.viewModel = viewModel + self.option = option + self.compositionInputAccessoryView = inputAccessoryView + + super.init(frame: .zero) + + initialSetup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension CompositionPollOptionView { + // swiftlint:disable:next function_body_length + func initialSetup() { + let stackView = UIStackView() + let textField = UITextField() + let remainingCharactersLabel = UILabel() + + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.spacing = .defaultSpacing + + stackView.addArrangedSubview(textField) + textField.borderStyle = .roundedRect + textField.adjustsFontForContentSizeCategory = true + textField.font = .preferredFont(forTextStyle: .body) + textField.inputAccessoryView = compositionInputAccessoryView + textField.addAction( + UIAction { [weak self] _ in + self?.option.text = textField.text ?? "" }, + for: .editingChanged) + + stackView.addArrangedSubview(remainingCharactersLabel) + remainingCharactersLabel.adjustsFontForContentSizeCategory = true + remainingCharactersLabel.font = .preferredFont(forTextStyle: .callout) + remainingCharactersLabel.setContentHuggingPriority(.required, for: .horizontal) + + stackView.addArrangedSubview(removeButton) + removeButton.showsMenuAsPrimaryAction = true + removeButton.menu = UIMenu( + children: [ + UIAction( + title: NSLocalizedString("remove", comment: ""), + image: UIImage(systemName: "trash"), + attributes: .destructive) { [weak self] _ in + guard let self = self else { return } + + self.viewModel.remove(pollOption: self.option) + }]) + removeButton.setContentHuggingPriority(.required, for: .horizontal) + removeButton.setContentHuggingPriority(.required, for: .vertical) + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + option.$remainingCharacters + .sink { + remainingCharactersLabel.text = String($0) + remainingCharactersLabel.textColor = $0 < 0 ? .systemRed : .label + } + .store(in: &cancellables) + } +} diff --git a/Views/CompositionPollView.swift b/Views/CompositionPollView.swift new file mode 100644 index 0000000..89f2327 --- /dev/null +++ b/Views/CompositionPollView.swift @@ -0,0 +1,146 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Combine +import UIKit +import ViewModels + +final class CompositionPollView: UIView { + private let viewModel: CompositionViewModel + private let compositionInputAccessoryView: CompositionInputAccessoryView + private let stackView = UIStackView() + private var cancellables = Set() + + init(viewModel: CompositionViewModel, inputAccessoryView: CompositionInputAccessoryView) { + self.viewModel = viewModel + self.compositionInputAccessoryView = inputAccessoryView + + super.init(frame: .zero) + + initialSetup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private extension CompositionPollView { + static let dateComponentsFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + + formatter.unitsStyle = .full + + return formatter + }() + + static func format(expiry: CompositionViewModel.PollExpiry) -> String? { + dateComponentsFormatter.string(from: TimeInterval(expiry.rawValue)) + } + + var pollOptionViews: [CompositionPollOptionView] { + stackView.arrangedSubviews.compactMap({ $0 as? CompositionPollOptionView }) + } + + // swiftlint:disable:next function_body_length + func initialSetup() { + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = .defaultSpacing + + let buttonsStackView = UIStackView() + + stackView.addArrangedSubview(buttonsStackView) + buttonsStackView.distribution = .fillEqually + + let addChoiceButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.viewModel.addPollOption() }) + + buttonsStackView.addArrangedSubview(addChoiceButton) + addChoiceButton.setImage( + UIImage(systemName: "plus", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), + for: .normal) + addChoiceButton.setTitle(NSLocalizedString("compose.poll.add-choice", comment: ""), for: .normal) + + let expiresInButton = UIButton(type: .system) + + buttonsStackView.addArrangedSubview(expiresInButton) + expiresInButton.setImage( + UIImage(systemName: "clock", + withConfiguration: UIImage.SymbolConfiguration(scale: .medium)), + for: .normal) + expiresInButton.showsMenuAsPrimaryAction = true + expiresInButton.menu = UIMenu(children: CompositionViewModel.PollExpiry.allCases.map { expiry in + UIAction(title: Self.format(expiry: expiry) ?? "") { [weak self] _ in + self?.viewModel.pollExpiresIn = expiry + } + }) + + let switchStackView = UIStackView() + + switchStackView.spacing = .defaultSpacing + + stackView.addArrangedSubview(switchStackView) + + let allowMultipleLabel = UILabel() + + switchStackView.addArrangedSubview(allowMultipleLabel) + allowMultipleLabel.adjustsFontForContentSizeCategory = true + allowMultipleLabel.font = .preferredFont(forTextStyle: .callout) + allowMultipleLabel.textColor = .secondaryLabel + allowMultipleLabel.text = NSLocalizedString("compose.poll.allow-multiple-choices", comment: "") + allowMultipleLabel.textAlignment = .right + + let allowMultipleSwitch = UISwitch() + + switchStackView.addArrangedSubview(allowMultipleSwitch) + allowMultipleSwitch.addAction( + UIAction { [weak self] _ in + self?.viewModel.sensitive = allowMultipleSwitch.isOn + }, + for: .valueChanged) + + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + buttonsStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension) + ]) + + viewModel.$pollOptions.sink { [weak self] in + guard let self = self else { return } + + addChoiceButton.isEnabled = $0.count < CompositionViewModel.maxPollOptionCount + + for (index, option) in $0.enumerated() { + if !self.pollOptionViews.contains(where: { $0.option === option }) { + let optionView = CompositionPollOptionView( + viewModel: self.viewModel, + option: option, + inputAccessoryView: self.compositionInputAccessoryView) + + self.stackView.insertArrangedSubview(optionView, at: index) + } + } + + for (index, optionView) in self.pollOptionViews.enumerated() { + optionView.removeButton.isHidden = index < CompositionViewModel.minPollOptionCount + + if !$0.contains(where: { $0 === optionView.option }) { + optionView.removeFromSuperview() + } + } + } + .store(in: &cancellables) + + viewModel.$pollExpiresIn + .sink { expiresInButton.setTitle(Self.format(expiry: $0), for: .normal) } + .store(in: &cancellables) + + viewModel.$pollMultipleChoice + .sink { allowMultipleSwitch.isEnabled = !$0 } + .store(in: &cancellables) + } +} diff --git a/Views/CompositionView.swift b/Views/CompositionView.swift index 5b17f15..39e2ad8 100644 --- a/Views/CompositionView.swift +++ b/Views/CompositionView.swift @@ -13,8 +13,10 @@ final class CompositionView: UIView { let removeButton = UIButton(type: .close) let inReplyToView = UIView() let hasReplyFollowingView = UIView() + let compositionInputAccessoryView: CompositionInputAccessoryView let attachmentsView = AttachmentsView() let attachmentUploadView: AttachmentUploadView + let pollView: CompositionPollView let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView private let viewModel: CompositionViewModel @@ -25,8 +27,12 @@ final class CompositionView: UIView { self.viewModel = viewModel self.parentViewModel = parentViewModel + compositionInputAccessoryView = CompositionInputAccessoryView( + viewModel: viewModel, + parentViewModel: parentViewModel) attachmentUploadView = AttachmentUploadView(viewModel: viewModel) markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel) + pollView = CompositionPollView(viewModel: viewModel, inputAccessoryView: compositionInputAccessoryView) super.init(frame: .zero) @@ -63,7 +69,6 @@ private extension CompositionView { avatarImageView.setContentHuggingPriority(.required, for: .horizontal) let stackView = UIStackView() - let inputAccessoryView = CompositionInputAccessoryView(viewModel: viewModel, parentViewModel: parentViewModel) addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false @@ -75,7 +80,7 @@ private extension CompositionView { spoilerTextField.adjustsFontForContentSizeCategory = true spoilerTextField.font = .preferredFont(forTextStyle: .body) spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "") - spoilerTextField.inputAccessoryView = inputAccessoryView + spoilerTextField.inputAccessoryView = compositionInputAccessoryView spoilerTextField.addAction( UIAction { [weak self] _ in guard let self = self, let text = self.spoilerTextField.text else { return } @@ -92,7 +97,7 @@ private extension CompositionView { textView.font = textViewFont textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 - textView.inputAccessoryView = inputAccessoryView + textView.inputAccessoryView = compositionInputAccessoryView textView.inputAccessoryView?.sizeToFit() textView.delegate = self @@ -109,6 +114,8 @@ private extension CompositionView { attachmentUploadView.isHidden = true stackView.addArrangedSubview(markAttachmentsSensitiveView) markAttachmentsSensitiveView.isHidden = true + stackView.addArrangedSubview(pollView) + pollView.isHidden = true addSubview(removeButton) removeButton.translatesAutoresizingMaskIntoConstraints = false @@ -172,6 +179,16 @@ private extension CompositionView { } .store(in: &cancellables) + viewModel.$displayPoll + .sink { [weak self] in + if !$0 { + self?.textView.becomeFirstResponder() + } + + self?.pollView.isHidden = !$0 + } + .store(in: &cancellables) + let guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide let constraints = [ avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),