From d05f97951b1a276d2eb3d50e88707da093c8294e Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Mar 2021 14:49:27 +0800 Subject: [PATCH] feat: add expires duration selector for poll --- Localization/app.json | 9 +++ Mastodon.xcodeproj/project.pbxproj | 12 ++- .../Diffiable/Item/ComposeStatusItem.swift | 64 ++++++++++++++-- .../Section/ComposeStatusSection.swift | 20 ++++- Mastodon/Generated/Strings.swift | 18 +++++ .../Resources/en.lproj/Localizable.strings | 7 ++ ...sPollExpiresOptionCollectionViewCell.swift | 74 +++++++++++++++++++ ...OptionAppendEntryCollectionViewCell.swift} | 20 ++--- .../Scene/Compose/ComposeViewController.swift | 31 +++++--- .../Compose/ComposeViewModel+Diffable.swift | 6 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 16 ++-- .../Share/View/Content/PollOptionView.swift | 3 +- 12 files changed, 234 insertions(+), 46 deletions(-) create mode 100644 Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift rename Mastodon/Scene/Compose/CollectionViewCell/{ComposeStatusNewPollOptionCollectionViewCell.swift => ComposeStatusPollOptionAppendEntryCollectionViewCell.swift} (80%) diff --git a/Localization/app.json b/Localization/app.json index 0cd2e7f83..3a3db1300 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -207,6 +207,15 @@ "attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.", "description_photo": "Describe photo for low vision people...", "description_video": "Describe what’s happening for low vision people..." + }, + "poll": { + "duration_time": "Duration: %s", + "thirty_minutes": "30 minutes", + "one_hour": "1 Hour", + "six_hours": "6 Hours", + "one_day": "1 Day", + "three_days": "3 Days", + "seven_days": "7 Days" } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 50f779c64..6b719bc71 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -123,6 +123,7 @@ DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; + DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; }; DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; @@ -189,7 +190,7 @@ DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; }; DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; }; DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; }; - DB87D4512609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift */; }; + DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */; }; DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; }; @@ -418,6 +419,7 @@ DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; + DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -487,7 +489,7 @@ DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; }; DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = ""; }; DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; - DB87D4502609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusNewPollOptionCollectionViewCell.swift; sourceTree = ""; }; + DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = ""; }; DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteBackwardResponseTextField.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = ""; }; @@ -1163,7 +1165,8 @@ DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */, DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */, DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */, - DB87D4502609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift */, + DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */, + DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */, ); path = CollectionViewCell; sourceTree = ""; @@ -1826,6 +1829,7 @@ DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, + DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, @@ -1953,7 +1957,7 @@ DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */, - DB87D4512609CF1E00D12C0D /* ComposeStatusNewPollOptionCollectionViewCell.swift in Sources */, + DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DB9A489026035963008B817C /* APIService+Media.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/ComposeStatusItem.swift b/Mastodon/Diffiable/Item/ComposeStatusItem.swift index 6d225e7d0..86e8c6228 100644 --- a/Mastodon/Diffiable/Item/ComposeStatusItem.swift +++ b/Mastodon/Diffiable/Item/ComposeStatusItem.swift @@ -14,8 +14,9 @@ enum ComposeStatusItem { case replyTo(statusObjectID: NSManagedObjectID) case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute) case attachment(attachmentService: MastodonAttachmentService) - case poll(attribute: ComposePollAttribute) - case newPoll + case pollOption(attribute: ComposePollOptionAttribute) + case pollOptionAppendEntry + case pollExpiresOption(attribute: ComposePollExpiresOptionAttribute) } extension ComposeStatusItem: Equatable { } @@ -44,16 +45,16 @@ extension ComposeStatusItem { } } -protocol ComposeStatusItemDelegate: class { - func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollAttribute, pollOptionDidChange: String?) +protocol ComposePollAttributeDelegate: class { + func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) } extension ComposeStatusItem { - final class ComposePollAttribute: Equatable, Hashable { + final class ComposePollOptionAttribute: Equatable, Hashable { private let id = UUID() var disposeBag = Set() - weak var delegate: ComposeStatusItemDelegate? + weak var delegate: ComposePollAttributeDelegate? let option = CurrentValueSubject("") @@ -70,7 +71,7 @@ extension ComposeStatusItem { disposeBag.removeAll() } - static func == (lhs: ComposePollAttribute, rhs: ComposePollAttribute) -> Bool { + static func == (lhs: ComposePollOptionAttribute, rhs: ComposePollOptionAttribute) -> Bool { return lhs.id == rhs.id && lhs.option.value == rhs.option.value } @@ -80,3 +81,52 @@ extension ComposeStatusItem { } } } + +extension ComposeStatusItem { + final class ComposePollExpiresOptionAttribute: Equatable, Hashable { + private let id = UUID() + + let expiresOption = CurrentValueSubject(.thirtyMinutes) + + + static func == (lhs: ComposePollExpiresOptionAttribute, rhs: ComposePollExpiresOptionAttribute) -> Bool { + return lhs.id == rhs.id && + lhs.expiresOption.value == rhs.expiresOption.value + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + enum ExpiresOption: Equatable, Hashable, CaseIterable { + case thirtyMinutes + case oneHour + case sixHours + case oneDay + case threeDays + case sevenDays + + var title: String { + switch self { + case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes + case .oneHour: return L10n.Scene.Compose.Poll.oneHour + case .sixHours: return L10n.Scene.Compose.Poll.sixHours + case .oneDay: return L10n.Scene.Compose.Poll.oneDay + case .threeDays: return L10n.Scene.Compose.Poll.threeDays + case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays + } + } + + var seconds: Int { + switch self { + case .thirtyMinutes: return 60 * 30 + case .oneHour: return 60 * 60 * 1 + case .sixHours: return 60 * 60 * 6 + case .oneDay: return 60 * 60 * 24 + case .threeDays: return 60 * 60 * 24 * 3 + case .sevenDays: return 60 * 60 * 24 * 7 + } + } + } + } +} diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index e8fee6d47..86bf9bd75 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -36,7 +36,8 @@ extension ComposeStatusSection { textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate, composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, - composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusNewPollOptionCollectionViewCellDelegate + composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, + composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate ) -> UICollectionViewDiffableDataSource { UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in switch item { @@ -127,7 +128,7 @@ extension ComposeStatusSection { } .store(in: &cell.disposeBag) return cell - case .poll(let attribute): + case .pollOption(let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell cell.pollOptionView.optionTextField.text = attribute.option.value cell.pollOption @@ -136,10 +137,21 @@ extension ComposeStatusSection { .store(in: &cell.disposeBag) cell.delegate = composeStatusPollOptionCollectionViewCellDelegate return cell - case .newPoll: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusNewPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusNewPollOptionCollectionViewCell + case .pollOptionAppendEntry: + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell cell.delegate = composeStatusNewPollOptionCollectionViewCellDelegate return cell + case .pollExpiresOption(let attribute): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell + cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) + attribute.expiresOption + .receive(on: DispatchQueue.main) + .sink { expiresOption in + cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) + } + .store(in: &cell.disposeBag) + cell.delegate = composeStatusPollExpiresOptionCollectionViewCellDelegate + return cell } } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7f142cb99..86c3bf13e 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -170,6 +170,24 @@ internal enum L10n { /// Photo Library internal static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary") } + internal enum Poll { + /// Duration: %@ + internal static func durationTime(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Compose.Poll.DurationTime", String(describing: p1)) + } + /// 1 Day + internal static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay") + /// 1 Hour + internal static let oneHour = L10n.tr("Localizable", "Scene.Compose.Poll.OneHour") + /// 7 Days + internal static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays") + /// 6 Hours + internal static let sixHours = L10n.tr("Localizable", "Scene.Compose.Poll.SixHours") + /// 30 minutes + internal static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes") + /// 3 Days + internal static let threeDays = L10n.tr("Localizable", "Scene.Compose.Poll.ThreeDays") + } internal enum Title { /// New Post internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 87649a3e0..dd34cbfe1 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -50,6 +50,13 @@ uploaded to Mastodon."; "Scene.Compose.MediaSelection.Browse" = "Browse"; "Scene.Compose.MediaSelection.Camera" = "Take Photo"; "Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library"; +"Scene.Compose.Poll.DurationTime" = "Duration: %@"; +"Scene.Compose.Poll.OneDay" = "1 Day"; +"Scene.Compose.Poll.OneHour" = "1 Hour"; +"Scene.Compose.Poll.SevenDays" = "7 Days"; +"Scene.Compose.Poll.SixHours" = "6 Hours"; +"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes"; +"Scene.Compose.Poll.ThreeDays" = "3 Days"; "Scene.Compose.Title.NewPost" = "New Post"; "Scene.Compose.Title.NewReply" = "New Reply"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift new file mode 100644 index 000000000..0abe94ba0 --- /dev/null +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift @@ -0,0 +1,74 @@ +// +// ComposeStatusPollExpiresOptionCollectionViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import os.log +import UIKit +import Combine + +protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: class { + func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) +} + +final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { + + var disposeBag = Set() + weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? + + let durationButton: UIButton = { + let button = HighlightDimmableButton() + button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12)) + button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20) + button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal) + button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ComposeStatusPollExpiresOptionCollectionViewCell { + + private typealias ExpiresOption = ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption + + private func _init() { + durationButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(durationButton) + NSLayoutConstraint.activate([ + durationButton.topAnchor.constraint(equalTo: contentView.topAnchor), + durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), + durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + let children = ExpiresOption.allCases.map { expiresOption -> UIAction in + UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in + guard let self = self else { return } + self.expiresOptionActionHandler(action, expiresOption: expiresOption) + } + } + durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) + durationButton.showsMenuAsPrimaryAction = true + } + +} + +extension ComposeStatusPollExpiresOptionCollectionViewCell { + + private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title) + delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption) + } + +} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusNewPollOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift similarity index 80% rename from Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusNewPollOptionCollectionViewCell.swift rename to Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift index 131af21e7..9479575b7 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusNewPollOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift @@ -1,5 +1,5 @@ // -// ComposeStatusNewPollOptionCollectionViewCell.swift +// ComposeStatusPollOptionAppendEntryCollectionViewCell.swift // Mastodon // // Created by MainasuK Cirno on 2021-3-23. @@ -8,11 +8,11 @@ import os.log import UIKit -protocol ComposeStatusNewPollOptionCollectionViewCellDelegate: class { - func ComposeStatusNewPollOptionCollectionViewCellDidPressed(_ cell: ComposeStatusNewPollOptionCollectionViewCell) +protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: class { + func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) } -final class ComposeStatusNewPollOptionCollectionViewCell: UICollectionViewCell { +final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionViewCell { let pollOptionView = PollOptionView() @@ -25,7 +25,7 @@ final class ComposeStatusNewPollOptionCollectionViewCell: UICollectionViewCell { } } - weak var delegate: ComposeStatusNewPollOptionCollectionViewCellDelegate? + weak var delegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? override func prepareForReuse() { super.prepareForReuse() @@ -45,7 +45,7 @@ final class ComposeStatusNewPollOptionCollectionViewCell: UICollectionViewCell { } -extension ComposeStatusNewPollOptionCollectionViewCell { +extension ComposeStatusPollOptionAppendEntryCollectionViewCell { private func _init() { pollOptionView.translatesAutoresizingMaskIntoConstraints = false @@ -67,7 +67,7 @@ extension ComposeStatusNewPollOptionCollectionViewCell { setupBorderColor() pollOptionView.addGestureRecognizer(singleTagGestureRecognizer) - singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusNewPollOptionCollectionViewCell.singleTagGestureRecognizerHandler(_:))) + singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionAppendEntryCollectionViewCell.singleTagGestureRecognizerHandler(_:))) } private func setupBorderColor() { @@ -83,11 +83,11 @@ extension ComposeStatusNewPollOptionCollectionViewCell { } -extension ComposeStatusNewPollOptionCollectionViewCell { +extension ComposeStatusPollOptionAppendEntryCollectionViewCell { @objc private func singleTagGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.ComposeStatusNewPollOptionCollectionViewCellDidPressed(self) + delegate?.composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(self) } } @@ -100,7 +100,7 @@ struct ComposeStatusNewPollOptionCollectionViewCell_Previews: PreviewProvider { static var controls: some View { Group { UIViewPreview() { - let cell = ComposeStatusNewPollOptionCollectionViewCell() + let cell = ComposeStatusPollOptionAppendEntryCollectionViewCell() return cell } .previewLayout(.fixed(width: 375, height: 44 + 10)) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 54de777ed..95398ca1b 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -46,7 +46,8 @@ final class ComposeViewController: UIViewController, NeedsDependency { collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self)) collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self)) collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self)) - collectionView.register(ComposeStatusNewPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusNewPollOptionCollectionViewCell.self)) + collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self)) + collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color return collectionView }() @@ -158,7 +159,8 @@ extension ComposeViewController { textEditorViewTextAttributesDelegate: self, composeStatusAttachmentTableViewCellDelegate: self, composeStatusPollOptionCollectionViewCellDelegate: self, - composeStatusNewPollOptionCollectionViewCellDelegate: self + composeStatusNewPollOptionCollectionViewCellDelegate: self, + composeStatusPollExpiresOptionCollectionViewCellDelegate: self ) // respond scrollView overlap change @@ -283,7 +285,7 @@ extension ComposeViewController { } private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? { - guard case .poll = item else { return nil } + guard case .pollOption = item else { return nil } guard let diffableDataSource = viewModel.diffableDataSource else { return nil } guard let indexPath = diffableDataSource.indexPath(for: item), let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else { @@ -297,7 +299,7 @@ extension ComposeViewController { guard let diffableDataSource = viewModel.diffableDataSource else { return nil } let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll) let firstPollItem = items.first { item -> Bool in - guard case .poll = item else { return false } + guard case .pollOption = item else { return false } return true } @@ -312,7 +314,7 @@ extension ComposeViewController { guard let diffableDataSource = viewModel.diffableDataSource else { return nil } let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll) let lastPollItem = items.last { item -> Bool in - guard case .poll = item else { return false } + guard case .pollOption = item else { return false } return true } @@ -570,7 +572,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate { // setup initial poll option if needs if viewModel.isPollComposing.value, viewModel.pollAttributes.value.isEmpty { - viewModel.pollAttributes.value = [ComposeStatusItem.ComposePollAttribute(), ComposeStatusItem.ComposePollAttribute()] + viewModel.pollAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()] } if viewModel.isPollComposing.value { @@ -704,7 +706,7 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let indexPath = collectionView.indexPath(for: cell) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - guard case let .poll(attribute) = item else { return } + guard case let .pollOption(attribute) = item else { return } var pollAttributes = viewModel.pollAttributes.value guard let index = pollAttributes.firstIndex(of: attribute) else { return } @@ -747,7 +749,7 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let indexPath = collectionView.indexPath(for: cell) else { return } let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll).filter { item in - guard case .poll = item else { return false } + guard case .pollOption = item else { return false } return true } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } @@ -770,12 +772,19 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega } -// MARK: - ComposeStatusNewPollOptionCollectionViewCellDelegate -extension ComposeViewController: ComposeStatusNewPollOptionCollectionViewCellDelegate { - func ComposeStatusNewPollOptionCollectionViewCellDidPressed(_ cell: ComposeStatusNewPollOptionCollectionViewCell) { +// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate +extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate { + func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) { viewModel.createNewPollOptionIfPossible() DispatchQueue.main.async { self.markLastPollOptionCollectionViewCellBecomeFirstResponser() } } } + +// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate +extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate { + func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) { + viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption + } +} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift index c838f3e25..bfca4a39f 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -16,7 +16,8 @@ extension ComposeViewModel { textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate, composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, - composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusNewPollOptionCollectionViewCellDelegate + composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, + composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate ) { let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource( for: collectionView, @@ -26,7 +27,8 @@ extension ComposeViewModel { textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate, composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate, - composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate + composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate, + composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate ) // Note: do not allow reorder due to the images display order following the upload time diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 4c49117f1..e423312ef 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -52,7 +52,8 @@ final class ComposeViewModel { let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([]) // polls - let pollAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollAttribute], Never>([]) + let pollAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([]) + let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute() init( context: AppContext, @@ -196,13 +197,14 @@ final class ComposeViewModel { if isPollComposing { var pollItems: [ComposeStatusItem] = [] for pollAttribute in pollAttributes { - let item = ComposeStatusItem.poll(attribute: pollAttribute) + let item = ComposeStatusItem.pollOption(attribute: pollAttribute) pollItems.append(item) } snapshot.appendItems(pollItems, toSection: .poll) if pollAttributes.count < 4 { - snapshot.appendItems([ComposeStatusItem.newPoll], toSection: .poll) + snapshot.appendItems([ComposeStatusItem.pollOptionAppendEntry], toSection: .poll) } + snapshot.appendItems([ComposeStatusItem.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)], toSection: .poll) } diffableDataSource.apply(snapshot) @@ -268,7 +270,7 @@ extension ComposeViewModel { func createNewPollOptionIfPossible() { guard pollAttributes.value.count < 4 else { return } - let attribute = ComposeStatusItem.ComposePollAttribute() + let attribute = ComposeStatusItem.ComposePollOptionAttribute() pollAttributes.value = pollAttributes.value + [attribute] } } @@ -281,9 +283,9 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate { } } -// MARK: - ComposeStatusAttributeDelegate -extension ComposeViewModel: ComposeStatusItemDelegate { - func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollAttribute, pollOptionDidChange: String?) { +// MARK: - ComposePollAttributeDelegate +extension ComposeViewModel: ComposePollAttributeDelegate { + func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) { // trigger update pollAttributes.value = pollAttributes.value } diff --git a/Mastodon/Scene/Share/View/Content/PollOptionView.swift b/Mastodon/Scene/Share/View/Content/PollOptionView.swift index 4e5e5a2ae..eafeb55cf 100644 --- a/Mastodon/Scene/Share/View/Content/PollOptionView.swift +++ b/Mastodon/Scene/Share/View/Content/PollOptionView.swift @@ -14,6 +14,7 @@ final class PollOptionView: UIView { static let optionHeight: CGFloat = 44 static let verticalMargin: CGFloat = 5 static let checkmarkImageSize = CGSize(width: 26, height: 26) + static let checkmarkBackgroundLeadingMargin: CGFloat = 9 private var viewStateDisposeBag = Set() @@ -105,7 +106,7 @@ extension PollOptionView { roundedBackgroundView.addSubview(checkmarkBackgroundView) NSLayoutConstraint.activate([ checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9), - checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9), + checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9), checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionView.checkmarkImageSize.width).priority(.required - 1), checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionView.checkmarkImageSize.height).priority(.required - 1),