mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
Post polls
This commit is contained in:
parent
377bf6aecc
commit
5c5677507b
9 changed files with 343 additions and 11 deletions
|
@ -44,11 +44,13 @@
|
||||||
"camera-access.open-system-settings" = "Open system settings";
|
"camera-access.open-system-settings" = "Open system settings";
|
||||||
"cancel" = "Cancel";
|
"cancel" = "Cancel";
|
||||||
"compose.attachment.uploading" = "Uploading";
|
"compose.attachment.uploading" = "Uploading";
|
||||||
"compose.prompt" = "What's on your mind?";
|
"compose.browse" = "Browse";
|
||||||
"compose.mark-media-sensitive" = "Mark media as sensitive";
|
"compose.mark-media-sensitive" = "Mark media as sensitive";
|
||||||
"compose.photo-library" = "Photo Library";
|
"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.take-photo-or-video" = "Take Photo or Video";
|
||||||
"compose.browse" = "Browse";
|
|
||||||
"error" = "Error";
|
"error" = "Error";
|
||||||
"favorites" = "Favorites";
|
"favorites" = "Favorites";
|
||||||
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
||||||
|
|
|
@ -23,6 +23,9 @@ public extension StatusEndpoint {
|
||||||
public let mediaIds: [Attachment.Id]
|
public let mediaIds: [Attachment.Id]
|
||||||
public let visibility: Status.Visibility
|
public let visibility: Status.Visibility
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
|
public let pollOptions: [String]
|
||||||
|
public let pollExpiresIn: Int
|
||||||
|
public let pollMultipleChoice: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
inReplyToId: Status.Id?,
|
inReplyToId: Status.Id?,
|
||||||
|
@ -30,13 +33,19 @@ public extension StatusEndpoint {
|
||||||
spoilerText: String,
|
spoilerText: String,
|
||||||
mediaIds: [Attachment.Id],
|
mediaIds: [Attachment.Id],
|
||||||
visibility: Status.Visibility,
|
visibility: Status.Visibility,
|
||||||
sensitive: Bool) {
|
sensitive: Bool,
|
||||||
|
pollOptions: [String],
|
||||||
|
pollExpiresIn: Int,
|
||||||
|
pollMultipleChoice: Bool) {
|
||||||
self.inReplyToId = inReplyToId
|
self.inReplyToId = inReplyToId
|
||||||
self.text = text
|
self.text = text
|
||||||
self.spoilerText = spoilerText
|
self.spoilerText = spoilerText
|
||||||
self.mediaIds = mediaIds
|
self.mediaIds = mediaIds
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.sensitive = sensitive
|
self.sensitive = sensitive
|
||||||
|
self.pollOptions = pollOptions
|
||||||
|
self.pollExpiresIn = pollExpiresIn
|
||||||
|
self.pollMultipleChoice = pollMultipleChoice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +73,16 @@ extension StatusEndpoint.Components {
|
||||||
params["sensitive"] = true
|
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
|
return params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,10 @@
|
||||||
D05936F525AA66A600754FDF /* 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 */; };
|
D05936FF25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */; };
|
||||||
D059370025AA94EA00754FDF /* 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 */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.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 = "<group>"; };
|
D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAttachmentView.swift; sourceTree = "<group>"; };
|
||||||
D05936F325AA66A600754FDF /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
|
D05936F325AA66A600754FDF /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAttachmentsSensitiveView.swift; sourceTree = "<group>"; };
|
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAttachmentsSensitiveView.swift; sourceTree = "<group>"; };
|
||||||
|
D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = "<group>"; };
|
||||||
|
D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.swift; sourceTree = "<group>"; };
|
||||||
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
||||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -450,6 +456,8 @@
|
||||||
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */,
|
||||||
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */,
|
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */,
|
||||||
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
|
D0E9F9A9258450B300EF503D /* CompositionInputAccessoryView.swift */,
|
||||||
|
D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */,
|
||||||
|
D059373225AAEA7000754FDF /* CompositionPollView.swift */,
|
||||||
D08E52ED257D757100FA2C5F /* CompositionView.swift */,
|
D08E52ED257D757100FA2C5F /* CompositionView.swift */,
|
||||||
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
||||||
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
||||||
|
@ -809,6 +817,7 @@
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
||||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||||
|
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
|
@ -824,6 +833,7 @@
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
|
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
||||||
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
||||||
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||||
|
@ -870,6 +880,7 @@
|
||||||
files = (
|
files = (
|
||||||
D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
D036EBC7259FE2B700EC1CFC /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
||||||
|
D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||||
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
|
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
|
||||||
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||||
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
|
@ -881,6 +892,7 @@
|
||||||
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
||||||
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||||
D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */,
|
D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */,
|
||||||
|
D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
|
||||||
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
||||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||||
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
||||||
|
|
|
@ -72,7 +72,6 @@ final class EditAttachmentViewController: UIViewController {
|
||||||
stackView.addArrangedSubview(remainingCharactersLabel)
|
stackView.addArrangedSubview(remainingCharactersLabel)
|
||||||
remainingCharactersLabel.adjustsFontForContentSizeCategory = true
|
remainingCharactersLabel.adjustsFontForContentSizeCategory = true
|
||||||
remainingCharactersLabel.font = .preferredFont(forTextStyle: .subheadline)
|
remainingCharactersLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
remainingCharactersLabel.text = "1500"
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
||||||
|
|
|
@ -12,6 +12,11 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
@Published public var contentWarning = ""
|
@Published public var contentWarning = ""
|
||||||
@Published public var displayContentWarning = false
|
@Published public var displayContentWarning = false
|
||||||
@Published public var sensitive = 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 attachmentViewModels = [AttachmentViewModel]()
|
||||||
@Published public private(set) var attachmentUpload: AttachmentUpload?
|
@Published public private(set) var attachmentUpload: AttachmentUpload?
|
||||||
@Published public private(set) var isPostable = false
|
@Published public private(set) var isPostable = false
|
||||||
|
@ -34,8 +39,8 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
}
|
}
|
||||||
.assign(to: &$isPostable)
|
.assign(to: &$isPostable)
|
||||||
$attachmentViewModels
|
$attachmentViewModels
|
||||||
.combineLatest($attachmentUpload)
|
.combineLatest($attachmentUpload, $displayPoll)
|
||||||
.map { $0.count < Self.maxAttachmentCount && $1 == nil }
|
.map { $0.count < Self.maxAttachmentCount && $1 == nil && !$2 }
|
||||||
.assign(to: &$canAddAttachment)
|
.assign(to: &$canAddAttachment)
|
||||||
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
|
$attachmentViewModels.map(\.isEmpty).assign(to: &$canAddNonImageAttachment)
|
||||||
$text.map {
|
$text.map {
|
||||||
|
@ -60,12 +65,35 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
|
|
||||||
public extension CompositionViewModel {
|
public extension CompositionViewModel {
|
||||||
static let maxCharacters = 500
|
static let maxCharacters = 500
|
||||||
|
static let minPollOptionCount = 2
|
||||||
|
static let maxPollOptionCount = 4
|
||||||
|
|
||||||
enum Event {
|
enum Event {
|
||||||
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
||||||
case updateAttachment(AnyPublisher<Never, Error>)
|
case updateAttachment(AnyPublisher<Never, Error>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
typealias Id = UUID
|
||||||
|
|
||||||
func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
|
func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
|
||||||
|
@ -75,7 +103,18 @@ public extension CompositionViewModel {
|
||||||
spoilerText: displayContentWarning ? contentWarning : "",
|
spoilerText: displayContentWarning ? contentWarning : "",
|
||||||
mediaIds: attachmentViewModels.map(\.attachment.id),
|
mediaIds: attachmentViewModels.map(\.attachment.id),
|
||||||
visibility: visibility,
|
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() {
|
func cancelUpload() {
|
||||||
|
@ -100,6 +139,12 @@ public extension CompositionViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension CompositionViewModel.PollOption {
|
||||||
|
static let maxCharacters = 25
|
||||||
|
|
||||||
|
typealias Id = UUID
|
||||||
|
}
|
||||||
|
|
||||||
extension CompositionViewModel {
|
extension CompositionViewModel {
|
||||||
func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) {
|
func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) {
|
||||||
attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider)
|
attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider)
|
||||||
|
|
|
@ -83,7 +83,7 @@ private extension CompositionInputAccessoryView {
|
||||||
attachmentButton.showsMenuAsPrimaryAction = true
|
attachmentButton.showsMenuAsPrimaryAction = true
|
||||||
attachmentButton.menu = UIMenu(children: attachmentActions)
|
attachmentButton.menu = UIMenu(children: attachmentActions)
|
||||||
|
|
||||||
let pollButton = UIButton()
|
let pollButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.viewModel.displayPoll.toggle() })
|
||||||
|
|
||||||
stackView.addArrangedSubview(pollButton)
|
stackView.addArrangedSubview(pollButton)
|
||||||
pollButton.setImage(
|
pollButton.setImage(
|
||||||
|
@ -134,6 +134,11 @@ private extension CompositionInputAccessoryView {
|
||||||
.sink { attachmentButton.isEnabled = $0 }
|
.sink { attachmentButton.isEnabled = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$attachmentViewModels
|
||||||
|
.combineLatest(viewModel.$attachmentUpload)
|
||||||
|
.sink { pollButton.isEnabled = $0.isEmpty && $1 == nil }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$remainingCharacters.sink {
|
viewModel.$remainingCharacters.sink {
|
||||||
charactersLabel.text = String($0)
|
charactersLabel.text = String($0)
|
||||||
charactersLabel.textColor = $0 < 0 ? .systemRed : .label
|
charactersLabel.textColor = $0 < 0 ? .systemRed : .label
|
||||||
|
|
87
Views/CompositionPollOptionView.swift
Normal file
87
Views/CompositionPollOptionView.swift
Normal file
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
146
Views/CompositionPollView.swift
Normal file
146
Views/CompositionPollView.swift
Normal file
|
@ -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<AnyCancellable>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,10 @@ final class CompositionView: UIView {
|
||||||
let removeButton = UIButton(type: .close)
|
let removeButton = UIButton(type: .close)
|
||||||
let inReplyToView = UIView()
|
let inReplyToView = UIView()
|
||||||
let hasReplyFollowingView = UIView()
|
let hasReplyFollowingView = UIView()
|
||||||
|
let compositionInputAccessoryView: CompositionInputAccessoryView
|
||||||
let attachmentsView = AttachmentsView()
|
let attachmentsView = AttachmentsView()
|
||||||
let attachmentUploadView: AttachmentUploadView
|
let attachmentUploadView: AttachmentUploadView
|
||||||
|
let pollView: CompositionPollView
|
||||||
let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView
|
let markAttachmentsSensitiveView: MarkAttachmentsSensitiveView
|
||||||
|
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: CompositionViewModel
|
||||||
|
@ -25,8 +27,12 @@ final class CompositionView: UIView {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.parentViewModel = parentViewModel
|
self.parentViewModel = parentViewModel
|
||||||
|
|
||||||
|
compositionInputAccessoryView = CompositionInputAccessoryView(
|
||||||
|
viewModel: viewModel,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
attachmentUploadView = AttachmentUploadView(viewModel: viewModel)
|
attachmentUploadView = AttachmentUploadView(viewModel: viewModel)
|
||||||
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
|
markAttachmentsSensitiveView = MarkAttachmentsSensitiveView(viewModel: viewModel)
|
||||||
|
pollView = CompositionPollView(viewModel: viewModel, inputAccessoryView: compositionInputAccessoryView)
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -63,7 +69,6 @@ private extension CompositionView {
|
||||||
avatarImageView.setContentHuggingPriority(.required, for: .horizontal)
|
avatarImageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
|
||||||
let stackView = UIStackView()
|
let stackView = UIStackView()
|
||||||
let inputAccessoryView = CompositionInputAccessoryView(viewModel: viewModel, parentViewModel: parentViewModel)
|
|
||||||
|
|
||||||
addSubview(stackView)
|
addSubview(stackView)
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -75,7 +80,7 @@ private extension CompositionView {
|
||||||
spoilerTextField.adjustsFontForContentSizeCategory = true
|
spoilerTextField.adjustsFontForContentSizeCategory = true
|
||||||
spoilerTextField.font = .preferredFont(forTextStyle: .body)
|
spoilerTextField.font = .preferredFont(forTextStyle: .body)
|
||||||
spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "")
|
spoilerTextField.placeholder = NSLocalizedString("status.spoiler-text-placeholder", comment: "")
|
||||||
spoilerTextField.inputAccessoryView = inputAccessoryView
|
spoilerTextField.inputAccessoryView = compositionInputAccessoryView
|
||||||
spoilerTextField.addAction(
|
spoilerTextField.addAction(
|
||||||
UIAction { [weak self] _ in
|
UIAction { [weak self] _ in
|
||||||
guard let self = self, let text = self.spoilerTextField.text else { return }
|
guard let self = self, let text = self.spoilerTextField.text else { return }
|
||||||
|
@ -92,7 +97,7 @@ private extension CompositionView {
|
||||||
textView.font = textViewFont
|
textView.font = textViewFont
|
||||||
textView.textContainerInset = .zero
|
textView.textContainerInset = .zero
|
||||||
textView.textContainer.lineFragmentPadding = 0
|
textView.textContainer.lineFragmentPadding = 0
|
||||||
textView.inputAccessoryView = inputAccessoryView
|
textView.inputAccessoryView = compositionInputAccessoryView
|
||||||
textView.inputAccessoryView?.sizeToFit()
|
textView.inputAccessoryView?.sizeToFit()
|
||||||
textView.delegate = self
|
textView.delegate = self
|
||||||
|
|
||||||
|
@ -109,6 +114,8 @@ private extension CompositionView {
|
||||||
attachmentUploadView.isHidden = true
|
attachmentUploadView.isHidden = true
|
||||||
stackView.addArrangedSubview(markAttachmentsSensitiveView)
|
stackView.addArrangedSubview(markAttachmentsSensitiveView)
|
||||||
markAttachmentsSensitiveView.isHidden = true
|
markAttachmentsSensitiveView.isHidden = true
|
||||||
|
stackView.addArrangedSubview(pollView)
|
||||||
|
pollView.isHidden = true
|
||||||
|
|
||||||
addSubview(removeButton)
|
addSubview(removeButton)
|
||||||
removeButton.translatesAutoresizingMaskIntoConstraints = false
|
removeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -172,6 +179,16 @@ private extension CompositionView {
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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 guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide
|
||||||
let constraints = [
|
let constraints = [
|
||||||
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
|
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
|
||||||
|
|
Loading…
Reference in a new issue