Merge pull request #74 from tootsuite/feature/compose-poll
Add poll supports for status publish
This commit is contained in:
commit
91cd7322e7
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */; };
|
||||
|
@ -187,6 +188,10 @@
|
|||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
|
||||
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 /* 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 */; };
|
||||
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -414,6 +419,7 @@
|
|||
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -481,6 +487,10 @@
|
|||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteBackwardResponseTextField.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -665,6 +675,7 @@
|
|||
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
||||
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
||||
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
|
||||
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
|
||||
);
|
||||
path = Content;
|
||||
sourceTree = "<group>";
|
||||
|
@ -752,7 +763,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||
DB49A61925FF327D00B98345 /* EmojiService */,
|
||||
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||
|
@ -843,6 +853,7 @@
|
|||
DB9D6C1325E4F97A0051B173 /* Container */,
|
||||
DBA9B90325F1D4420012E7B6 /* Control */,
|
||||
2D152A8A25C295B8009AA50C /* Content */,
|
||||
DB87D45C2609DE6600D12C0D /* TextField */,
|
||||
DB1D187125EF5BBD003F1F23 /* TableView */,
|
||||
2D7631A625C1533800929FB9 /* TableviewCell */,
|
||||
);
|
||||
|
@ -1153,10 +1164,21 @@
|
|||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
|
||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
|
||||
);
|
||||
path = CollectionViewCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB87D45C2609DE6600D12C0D /* TextField */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */,
|
||||
);
|
||||
path = TextField;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1792,6 +1814,7 @@
|
|||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||
|
@ -1806,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 */,
|
||||
|
@ -1818,6 +1842,7 @@
|
|||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
|
||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
||||
|
@ -1868,6 +1893,7 @@
|
|||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||
|
@ -1931,6 +1957,7 @@
|
|||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.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 */,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum CategoryPickerItem {
|
||||
case all
|
||||
case category(category: Mastodon.Entity.Category)
|
||||
|
|
|
@ -9,18 +9,24 @@ import Foundation
|
|||
import Combine
|
||||
import CoreData
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum ComposeStatusItem {
|
||||
case replyTo(statusObjectID: NSManagedObjectID)
|
||||
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
||||
case attachment(attachmentService: MastodonAttachmentService)
|
||||
case pollOption(attribute: ComposePollOptionAttribute)
|
||||
case pollOptionAppendEntry
|
||||
case pollExpiresOption(attribute: ComposePollExpiresOptionAttribute)
|
||||
}
|
||||
|
||||
extension ComposeStatusItem: Equatable { }
|
||||
|
||||
extension ComposeStatusItem: Hashable { }
|
||||
|
||||
extension ComposeStatusItem {
|
||||
final class ComposeStatusAttribute: Equatable, Hashable {
|
||||
private let id = UUID()
|
||||
|
||||
|
||||
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
|
||||
let displayName = CurrentValueSubject<String?, Never>(nil)
|
||||
let username = CurrentValueSubject<String?, Never>(nil)
|
||||
|
@ -38,3 +44,89 @@ extension ComposeStatusItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol ComposePollAttributeDelegate: class {
|
||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?)
|
||||
}
|
||||
|
||||
extension ComposeStatusItem {
|
||||
final class ComposePollOptionAttribute: Equatable, Hashable {
|
||||
private let id = UUID()
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
weak var delegate: ComposePollAttributeDelegate?
|
||||
|
||||
let option = CurrentValueSubject<String, Never>("")
|
||||
|
||||
init() {
|
||||
option
|
||||
.sink { [weak self] option in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.composePollAttribute(self, pollOptionDidChange: option)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
deinit {
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
static func == (lhs: ComposePollOptionAttribute, rhs: ComposePollOptionAttribute) -> Bool {
|
||||
return lhs.id == rhs.id &&
|
||||
lhs.option.value == rhs.option.value
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeStatusItem {
|
||||
final class ComposePollExpiresOptionAttribute: Equatable, Hashable {
|
||||
private let id = UUID()
|
||||
|
||||
let expiresOption = CurrentValueSubject<ExpiresOption, Never>(.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import CoreData
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum PollItem {
|
||||
case opion(objectID: NSManagedObjectID, attribute: Attribute)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ enum ComposeStatusSection: Equatable, Hashable {
|
|||
case repliedTo
|
||||
case status
|
||||
case attachment
|
||||
case poll
|
||||
}
|
||||
|
||||
extension ComposeStatusSection {
|
||||
|
@ -33,7 +34,10 @@ extension ComposeStatusSection {
|
|||
managedObjectContext: NSManagedObjectContext,
|
||||
composeKind: ComposeKind,
|
||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
switch item {
|
||||
|
@ -54,11 +58,11 @@ extension ComposeStatusSection {
|
|||
}
|
||||
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||
// self size input cell
|
||||
cell.composeContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { text in
|
||||
// self size input cell
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
// bind input data
|
||||
attribute.composeContent.value = text
|
||||
|
@ -124,6 +128,30 @@ extension ComposeStatusSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
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
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: attribute.option)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||
return cell
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ extension PollSection {
|
|||
pollOption option: PollOption,
|
||||
pollItemAttribute attribute: PollItem.Attribute
|
||||
) {
|
||||
cell.optionLabel.text = option.title
|
||||
cell.pollOptionView.optionTextField.text = option.title
|
||||
configure(cell: cell, selectState: attribute.selectState)
|
||||
configure(cell: cell, voteState: attribute.voteState)
|
||||
cell.attribute = attribute
|
||||
|
@ -52,35 +52,35 @@ extension PollSection {
|
|||
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
|
||||
switch state {
|
||||
case .none:
|
||||
cell.checkmarkBackgroundView.isHidden = true
|
||||
cell.checkmarkImageView.isHidden = true
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = true
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||
case .off:
|
||||
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||
cell.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||
cell.checkmarkBackgroundView.layer.borderWidth = 1
|
||||
cell.checkmarkBackgroundView.isHidden = false
|
||||
cell.checkmarkImageView.isHidden = true
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||
case .on:
|
||||
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||
cell.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
||||
cell.checkmarkBackgroundView.layer.borderWidth = 0
|
||||
cell.checkmarkBackgroundView.isHidden = false
|
||||
cell.checkmarkImageView.isHidden = false
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
|
||||
switch state {
|
||||
case .hidden:
|
||||
cell.optionPercentageLabel.isHidden = true
|
||||
cell.voteProgressStripView.isHidden = true
|
||||
cell.voteProgressStripView.setProgress(0.0, animated: false)
|
||||
cell.pollOptionView.optionPercentageLabel.isHidden = true
|
||||
cell.pollOptionView.voteProgressStripView.isHidden = true
|
||||
cell.pollOptionView.voteProgressStripView.setProgress(0.0, animated: false)
|
||||
case .reveal(let voted, let percentage, let animated):
|
||||
cell.optionPercentageLabel.isHidden = false
|
||||
cell.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
||||
cell.voteProgressStripView.isHidden = false
|
||||
cell.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
|
||||
cell.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
|
||||
cell.pollOptionView.optionPercentageLabel.isHidden = false
|
||||
cell.pollOptionView.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
||||
cell.pollOptionView.voteProgressStripView.isHidden = false
|
||||
cell.pollOptionView.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
|
||||
cell.pollOptionView.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ internal enum Asset {
|
|||
}
|
||||
internal enum Circles {
|
||||
internal static let plusCircleFill = ImageAsset(name: "Circles/plus.circle.fill")
|
||||
internal static let plusCircle = ImageAsset(name: "Circles/plus.circle")
|
||||
}
|
||||
internal enum Colors {
|
||||
internal enum Background {
|
||||
|
@ -46,6 +47,7 @@ internal enum Asset {
|
|||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
||||
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
||||
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
||||
}
|
||||
internal enum Button {
|
||||
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
||||
|
|
|
@ -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")
|
||||
|
|
15
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/Contents.json
vendored
Normal file
15
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "plus.circle.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
95
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/plus.circle.pdf
vendored
Normal file
95
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/plus.circle.pdf
vendored
Normal file
|
@ -0,0 +1,95 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
21.999905 0.000160 m
|
||||
34.035152 0.000160 44.000000 9.986404 44.000000 22.000080 c
|
||||
44.000000 34.035332 34.013599 44.000000 21.978350 44.000000 c
|
||||
9.964656 44.000000 0.000000 34.035332 0.000000 22.000080 c
|
||||
0.000000 9.986404 9.986255 0.000160 21.999905 0.000160 c
|
||||
h
|
||||
21.999905 3.666824 m
|
||||
11.819542 3.666824 3.688203 11.819717 3.688203 22.000080 c
|
||||
3.688203 32.180443 11.797986 40.333336 21.978350 40.333336 c
|
||||
32.158710 40.333336 40.311611 32.180443 40.333256 22.000080 c
|
||||
40.354897 11.819717 32.180267 3.666824 21.999905 3.666824 c
|
||||
h
|
||||
13.782296 20.188307 m
|
||||
20.166574 20.188307 l
|
||||
20.166574 13.760918 l
|
||||
20.166574 12.682493 20.899923 11.949142 21.956793 11.949142 c
|
||||
23.035217 11.949142 23.790121 12.682493 23.790121 13.760918 c
|
||||
23.790121 20.188307 l
|
||||
30.217514 20.188307 l
|
||||
31.295938 20.188307 32.029289 20.921658 32.029289 21.978525 c
|
||||
32.029289 23.056950 31.295938 23.811855 30.217514 23.811855 c
|
||||
23.790121 23.811855 l
|
||||
23.790121 30.196133 l
|
||||
23.790121 31.317715 23.035217 32.051018 21.956793 32.051018 c
|
||||
20.899923 32.051018 20.166574 31.296114 20.166574 30.196133 c
|
||||
20.166574 23.811855 l
|
||||
13.782296 23.811855 l
|
||||
12.660716 23.811855 11.927410 23.056950 11.927410 21.978525 c
|
||||
11.927410 20.921658 12.682316 20.188307 13.782296 20.188307 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1347
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 44.000000 44.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001437 00000 n
|
||||
0000001460 00000 n
|
||||
0000001633 00000 n
|
||||
0000001707 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1766
|
||||
%%EOF
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "255",
|
||||
"green" : "255",
|
||||
"red" : "255"
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.169",
|
||||
"green" : "0.141",
|
||||
"red" : "0.125"
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x23",
|
||||
"red" : "0x1F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "232",
|
||||
"green" : "225",
|
||||
"red" : "217"
|
||||
"blue" : "0xE8",
|
||||
"green" : "0xE1",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.169",
|
||||
"green" : "0.141",
|
||||
"red" : "0.125"
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x23",
|
||||
"red" : "0x1F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,27 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x43",
|
||||
"green" : "0x36",
|
||||
"red" : "0x32"
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x23",
|
||||
"red" : "0x1F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xE8",
|
||||
"green" : "0xE1",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2B",
|
||||
"green" : "0x23",
|
||||
"red" : "0x1F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -66,7 +66,7 @@ extension ComposeStatusContentCollectionViewCell {
|
|||
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
|
||||
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 10),
|
||||
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// ComposeStatusPollOptionAppendEntryCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-23.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: class {
|
||||
func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell)
|
||||
}
|
||||
|
||||
final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
let pollOptionView = PollOptionView()
|
||||
let reorderBarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
|
||||
weak var delegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate?
|
||||
|
||||
override var isHighlighted: Bool {
|
||||
didSet {
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color
|
||||
pollOptionView.plusCircleImageView.tintColor = isHighlighted ? Asset.Colors.Button.normal.color.withAlphaComponent(0.5) : Asset.Colors.Button.normal.color
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return pollOptionView.frame.contains(point)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
delegate = nil
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
|
||||
|
||||
private func _init() {
|
||||
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(pollOptionView)
|
||||
NSLayoutConstraint.activate([
|
||||
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
reorderBarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(reorderBarImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
reorderBarImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
reorderBarImageView.leadingAnchor.constraint(equalTo: pollOptionView.trailingAnchor, constant: ComposeStatusPollOptionCollectionViewCell.reorderHandlerImageLeadingMargin),
|
||||
reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
pollOptionView.checkmarkImageView.isHidden = true
|
||||
pollOptionView.checkmarkBackgroundView.isHidden = true
|
||||
pollOptionView.optionPercentageLabel.isHidden = true
|
||||
pollOptionView.optionTextField.isHidden = true
|
||||
pollOptionView.plusCircleImageView.isHidden = false
|
||||
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
setupBorderColor()
|
||||
|
||||
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
|
||||
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionAppendEntryCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
|
||||
|
||||
reorderBarImageView.isHidden = true
|
||||
}
|
||||
|
||||
private func setupBorderColor() {
|
||||
pollOptionView.roundedBackgroundView.layer.borderWidth = 1
|
||||
pollOptionView.roundedBackgroundView.layer.borderColor = Asset.Colors.Background.secondarySystemBackground.color.cgColor
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
setupBorderColor()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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?.composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeStatusNewPollOptionCollectionViewCell_Previews: PreviewProvider {
|
||||
|
||||
static var controls: some View {
|
||||
Group {
|
||||
UIViewPreview() {
|
||||
let cell = ComposeStatusPollOptionAppendEntryCollectionViewCell()
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
controls.colorScheme(.light)
|
||||
controls.colorScheme(.dark)
|
||||
}
|
||||
.background(Color.gray)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,168 @@
|
|||
//
|
||||
// ComposeStatusPollOptionCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-23.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?)
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField)
|
||||
}
|
||||
|
||||
final class ComposeStatusPollOptionCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
static let reorderHandlerImageLeadingMargin: CGFloat = 11
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
weak var delegate: ComposeStatusPollOptionCollectionViewCellDelegate?
|
||||
|
||||
let pollOptionView = PollOptionView()
|
||||
let reorderBarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
|
||||
private var pollOptionSubscription: AnyCancellable?
|
||||
let pollOption = PassthroughSubject<String, Never>()
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return pollOptionView.frame.contains(point)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
delegate = nil
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollOptionCollectionViewCell {
|
||||
|
||||
private func _init() {
|
||||
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(pollOptionView)
|
||||
NSLayoutConstraint.activate([
|
||||
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
reorderBarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(reorderBarImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
reorderBarImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
reorderBarImageView.leadingAnchor.constraint(equalTo: pollOptionView.trailingAnchor, constant: ComposeStatusPollOptionCollectionViewCell.reorderHandlerImageLeadingMargin),
|
||||
reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
pollOptionView.checkmarkImageView.isHidden = true
|
||||
pollOptionView.optionPercentageLabel.isHidden = true
|
||||
pollOptionView.optionTextField.text = nil
|
||||
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color
|
||||
setupBorderColor()
|
||||
|
||||
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
|
||||
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
|
||||
|
||||
pollOptionSubscription = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: pollOptionView.optionTextField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] notification in
|
||||
guard let self = self else { return }
|
||||
guard let textField = notification.object as? UITextField else { return }
|
||||
self.pollOption.send(textField.text ?? "")
|
||||
}
|
||||
pollOptionView.optionTextField.deleteBackwardDelegate = self
|
||||
pollOptionView.optionTextField.delegate = self
|
||||
}
|
||||
|
||||
private func setupBorderColor() {
|
||||
pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||
pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
setupBorderColor()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollOptionCollectionViewCell {
|
||||
|
||||
@objc private func singleTagGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
pollOptionView.optionTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - DeleteBackwardResponseTextFieldDelegate
|
||||
extension ComposeStatusPollOptionCollectionViewCell: DeleteBackwardResponseTextFieldDelegate {
|
||||
func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) {
|
||||
delegate?.composeStatusPollOptionCollectionViewCell(self, textBeforeDeleteBackward: textBeforeDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
extension ComposeStatusPollOptionCollectionViewCell: UITextFieldDelegate {
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
if textField === pollOptionView.optionTextField {
|
||||
delegate?.composeStatusPollOptionCollectionViewCell(self, pollOptionTextFieldDidReturn: textField)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeStatusPollOptionCollectionViewCell_Previews: PreviewProvider {
|
||||
|
||||
static var controls: some View {
|
||||
Group {
|
||||
UIViewPreview() {
|
||||
let cell = ComposeStatusPollOptionCollectionViewCell()
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
controls.colorScheme(.light)
|
||||
controls.colorScheme(.dark)
|
||||
}
|
||||
.background(Color.gray)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -45,6 +45,9 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
|
||||
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(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
|
||||
}()
|
||||
|
@ -147,15 +150,17 @@ extension ComposeViewController {
|
|||
])
|
||||
|
||||
collectionView.delegate = self
|
||||
// Note: do not allow reorder due to the images display order following the upload time
|
||||
// let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
||||
// collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: collectionView,
|
||||
dependency: self,
|
||||
textEditorViewTextAttributesDelegate: self,
|
||||
composeStatusAttachmentTableViewCellDelegate: self
|
||||
composeStatusAttachmentTableViewCellDelegate: self,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: self
|
||||
)
|
||||
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
||||
collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||
|
||||
// respond scrollView overlap change
|
||||
view.layoutIfNeeded()
|
||||
|
@ -205,6 +210,16 @@ extension ComposeViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.isMediaToolbarButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.isPollToolbarButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind custom emojis
|
||||
viewModel.customEmojiViewModel
|
||||
|
@ -268,6 +283,57 @@ extension ComposeViewController {
|
|||
textEditorView()?.isEditing = true
|
||||
}
|
||||
|
||||
private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
||||
let firstPollItem = items.first { item -> Bool in
|
||||
guard case .pollOption = item else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
guard let item = firstPollItem else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return pollOptionCollectionViewCell(of: item)
|
||||
}
|
||||
|
||||
private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
||||
let lastPollItem = items.last { item -> Bool in
|
||||
guard case .pollOption = item else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
guard let item = lastPollItem else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return pollOptionCollectionViewCell(of: item)
|
||||
}
|
||||
|
||||
private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() {
|
||||
guard let cell = firstPollOptionCollectionViewCell() else { return }
|
||||
cell.pollOptionView.optionTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
|
||||
guard let cell = lastPollOptionCollectionViewCell() else { return }
|
||||
cell.pollOptionView.optionTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
private func showDismissConfirmAlertController() {
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.DiscardPostContent.title,
|
||||
|
@ -322,13 +388,20 @@ extension ComposeViewController {
|
|||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/* Do not allow reorder image due to image display order following the update time
|
||||
// seealso: ComposeViewModel.setupDiffableDataSource(…)
|
||||
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
||||
switch(sender.state) {
|
||||
case .began:
|
||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)) else {
|
||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||
let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
||||
break
|
||||
}
|
||||
// check if pressing reorder bar no not
|
||||
let locationInCell = sender.location(in: cell)
|
||||
guard cell.reorderBarImageView.frame.contains(locationInCell) else {
|
||||
return
|
||||
}
|
||||
|
||||
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
||||
case .changed:
|
||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||
|
@ -336,19 +409,20 @@ extension ComposeViewController {
|
|||
break
|
||||
}
|
||||
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
||||
case .attachment = item else {
|
||||
case .pollOption = item else {
|
||||
collectionView.cancelInteractiveMovement()
|
||||
return
|
||||
}
|
||||
|
||||
collectionView.updateInteractiveMovementTargetPosition(sender.location(in: collectionView))
|
||||
var position = sender.location(in: collectionView)
|
||||
position.x = collectionView.frame.width * 0.5
|
||||
collectionView.updateInteractiveMovementTargetPosition(position)
|
||||
case .ended:
|
||||
collectionView.endInteractiveMovement()
|
||||
default:
|
||||
collectionView.cancelInteractiveMovement()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
@ -490,7 +564,6 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, mediaSelectionType.rawValue)
|
||||
switch mediaSelectionType {
|
||||
case .photoLibrary:
|
||||
present(imagePicker, animated: true, completion: nil)
|
||||
|
@ -501,20 +574,31 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton) {
|
||||
viewModel.isPollComposing.value.toggle()
|
||||
|
||||
// setup initial poll option if needs
|
||||
if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty {
|
||||
viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()]
|
||||
}
|
||||
|
||||
if viewModel.isPollComposing.value {
|
||||
// Magic RunLoop
|
||||
DispatchQueue.main.async {
|
||||
self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
|
||||
}
|
||||
} else {
|
||||
markTextEditorViewBecomeFirstResponser()
|
||||
}
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) {
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton) {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -554,7 +638,6 @@ extension ComposeViewController: PHPickerViewControllerDelegate {
|
|||
pickerResult: result,
|
||||
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||
)
|
||||
service.delegate = viewModel
|
||||
return service
|
||||
}
|
||||
viewModel.attachmentServices.value = viewModel.attachmentServices.value + attachmentServices
|
||||
|
@ -574,7 +657,6 @@ extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationC
|
|||
image: image,
|
||||
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||
)
|
||||
attachmentService.delegate = viewModel
|
||||
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
||||
}
|
||||
|
||||
|
@ -598,7 +680,6 @@ extension ComposeViewController: UIDocumentPickerDelegate {
|
|||
imageData: imageData,
|
||||
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||
)
|
||||
attachmentService.delegate = viewModel
|
||||
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
||||
} catch {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
|
@ -622,3 +703,95 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
|
||||
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
|
||||
|
||||
// handle delete backward event for poll option input
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
||||
guard (text ?? "").isEmpty else { return }
|
||||
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 .pollOption(attribute) = item else { return }
|
||||
|
||||
var pollAttributes = viewModel.pollOptionAttributes.value
|
||||
guard let index = pollAttributes.firstIndex(of: attribute) else { return }
|
||||
|
||||
// mark previous (fallback to next) item of removed middle poll option become first responder
|
||||
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
||||
if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
|
||||
func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard index > 0 else { return nil }
|
||||
let indexBeforeRemoved = pollItems.index(before: indexOfItem)
|
||||
let itemBeforeRemoved = pollItems[indexBeforeRemoved]
|
||||
return pollOptionCollectionViewCell(of: itemBeforeRemoved)
|
||||
}
|
||||
|
||||
func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard index < pollItems.count - 1 else { return nil }
|
||||
let indexAfterRemoved = pollItems.index(after: index)
|
||||
let itemAfterRemoved = pollItems[indexAfterRemoved]
|
||||
return pollOptionCollectionViewCell(of: itemAfterRemoved)
|
||||
}
|
||||
|
||||
var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved()
|
||||
if cell == nil {
|
||||
cell = cellAfterRemoved()
|
||||
}
|
||||
cell?.pollOptionView.optionTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
guard pollAttributes.count > 2 else {
|
||||
return
|
||||
}
|
||||
pollAttributes.remove(at: index)
|
||||
|
||||
// update data source
|
||||
viewModel.pollOptionAttributes.value = pollAttributes
|
||||
}
|
||||
|
||||
// handle keyboard return event for poll option input
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
|
||||
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 .pollOption = item else { return false }
|
||||
return true
|
||||
}
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
guard let index = pollItems.firstIndex(of: item) else { return }
|
||||
|
||||
if index == pollItems.count - 1 {
|
||||
// is the last
|
||||
viewModel.createNewPollOptionIfPossible()
|
||||
DispatchQueue.main.async {
|
||||
self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
|
||||
}
|
||||
} else {
|
||||
// not the last
|
||||
let indexAfter = pollItems.index(after: index)
|
||||
let itemAfter = pollItems[indexAfter]
|
||||
let cell = pollOptionCollectionViewCell(of: itemAfter)
|
||||
cell?.pollOptionView.optionTextField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,10 @@ extension ComposeViewModel {
|
|||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency,
|
||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
) {
|
||||
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
|
||||
for: collectionView,
|
||||
|
@ -22,33 +25,36 @@ extension ComposeViewModel {
|
|||
managedObjectContext: context.managedObjectContext,
|
||||
composeKind: composeKind,
|
||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate
|
||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
)
|
||||
|
||||
// Note: do not allow reorder due to the images display order following the upload time
|
||||
// diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
||||
// switch item {
|
||||
// case .attachment: return true
|
||||
// default: return false
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||
// guard let self = self else { return }
|
||||
//
|
||||
// let items = transaction.finalSnapshot.itemIdentifiers
|
||||
// var attachmentServices: [MastodonAttachmentService] = []
|
||||
// for item in items {
|
||||
// guard case let .attachment(attachmentService) = item else { continue }
|
||||
// attachmentServices.append(attachmentService)
|
||||
// }
|
||||
// self.attachmentServices.value = attachmentServices
|
||||
// }
|
||||
//
|
||||
diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
||||
switch item {
|
||||
case .pollOption: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
// update reordered data source
|
||||
diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||
guard let self = self else { return }
|
||||
|
||||
let items = transaction.finalSnapshot.itemIdentifiers
|
||||
var pollOptionAttributes: [ComposeStatusItem.ComposePollOptionAttribute] = []
|
||||
for item in items {
|
||||
guard case let .pollOption(attribute) = item else { continue }
|
||||
pollOptionAttributes.append(attribute)
|
||||
}
|
||||
self.pollOptionAttributes.value = pollOptionAttributes
|
||||
}
|
||||
|
||||
|
||||
self.diffableDataSource = diffableDataSource
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||
snapshot.appendSections([.repliedTo, .status, .attachment])
|
||||
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
||||
switch composeKind {
|
||||
case .reply(let statusObjectID):
|
||||
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
||||
|
|
|
@ -53,6 +53,15 @@ extension ComposeViewModel.PublishState {
|
|||
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
||||
attachmentService.attachment.value?.id
|
||||
}
|
||||
let pollOptions: [String]? = {
|
||||
guard viewModel.isPollComposing.value else { return nil }
|
||||
return viewModel.pollOptionAttributes.value.map { attribute in attribute.option.value }
|
||||
}()
|
||||
let pollExpiresIn: Int? = {
|
||||
guard viewModel.isPollComposing.value else { return nil }
|
||||
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||
}()
|
||||
|
||||
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
||||
for attachmentService in attachmentServices {
|
||||
|
@ -81,7 +90,9 @@ extension ComposeViewModel.PublishState {
|
|||
.flatMap { attachments -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||
let query = Mastodon.API.Statuses.PublishStatusQuery(
|
||||
status: viewModel.composeStatusAttribute.composeContent.value,
|
||||
mediaIDs: mediaIDs
|
||||
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||
pollOptions: pollOptions,
|
||||
pollExpiresIn: pollExpiresIn
|
||||
)
|
||||
return viewModel.context.apiService.publishStatus(
|
||||
domain: domain,
|
||||
|
|
|
@ -19,6 +19,7 @@ final class ComposeViewModel {
|
|||
let context: AppContext
|
||||
let composeKind: ComposeStatusSection.ComposeKind
|
||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||
|
||||
|
@ -41,6 +42,8 @@ final class ComposeViewModel {
|
|||
let title: CurrentValueSubject<String, Never>
|
||||
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
||||
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
|
||||
// custom emojis
|
||||
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||
|
@ -48,6 +51,10 @@ final class ComposeViewModel {
|
|||
// attachment
|
||||
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||
|
||||
// polls
|
||||
let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([])
|
||||
let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute()
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
composeKind: ComposeStatusSection.ComposeKind
|
||||
|
@ -98,19 +105,48 @@ final class ComposeViewModel {
|
|||
.map { services in
|
||||
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
||||
}
|
||||
Publishers.CombineLatest4(
|
||||
let isPollAttributeAllValid = pollOptionAttributes
|
||||
.map { pollAttributes in
|
||||
pollAttributes.allSatisfy { attribute -> Bool in
|
||||
!attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
|
||||
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||
isComposeContentValid.eraseToAnyPublisher(),
|
||||
isMediaEmpty.eraseToAnyPublisher(),
|
||||
isMediaUploadAllSuccess.eraseToAnyPublisher()
|
||||
)
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess in
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
|
||||
if isMediaEmpty {
|
||||
return isComposeContentValid && !isComposeContentEmpty
|
||||
} else {
|
||||
return isComposeContentValid && isMediaUploadAllSuccess
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
|
||||
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||
isComposeContentValid.eraseToAnyPublisher(),
|
||||
isPollComposing.eraseToAnyPublisher(),
|
||||
isPollAttributeAllValid.eraseToAnyPublisher()
|
||||
)
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
|
||||
if isPollComposing {
|
||||
return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
|
||||
} else {
|
||||
return isComposeContentValid && !isComposeContentEmpty
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isPublishBarButtonItemEnabledPrecondition1,
|
||||
isPublishBarButtonItemEnabledPrecondition2
|
||||
)
|
||||
.map { $0 && $1 }
|
||||
.assign(to: \.value, on: isPublishBarButtonItemEnabled)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -137,49 +173,108 @@ final class ComposeViewModel {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind snapshot and drive service upload state
|
||||
attachmentServices
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
|
||||
var items: [ComposeStatusItem] = []
|
||||
for attachmentService in attachmentServices {
|
||||
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
|
||||
items.append(item)
|
||||
// bind snapshot
|
||||
Publishers.CombineLatest3(
|
||||
attachmentServices.eraseToAnyPublisher(),
|
||||
isPollComposing.eraseToAnyPublisher(),
|
||||
pollOptionAttributes.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices, isPollComposing, pollAttributes in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
|
||||
var attachmentItems: [ComposeStatusItem] = []
|
||||
for attachmentService in attachmentServices {
|
||||
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
|
||||
attachmentItems.append(item)
|
||||
}
|
||||
snapshot.appendItems(attachmentItems, toSection: .attachment)
|
||||
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .poll))
|
||||
if isPollComposing {
|
||||
var pollItems: [ComposeStatusItem] = []
|
||||
for pollAttribute in pollAttributes {
|
||||
let item = ComposeStatusItem.pollOption(attribute: pollAttribute)
|
||||
pollItems.append(item)
|
||||
}
|
||||
snapshot.appendItems(items, toSection: .attachment)
|
||||
|
||||
diffableDataSource.apply(snapshot)
|
||||
|
||||
// make image upload in the queue
|
||||
for attachmentService in attachmentServices {
|
||||
// skip when prefix N task when task finish OR fail OR uploading
|
||||
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
|
||||
if currentState is MastodonAttachmentService.UploadState.Fail {
|
||||
continue
|
||||
}
|
||||
if currentState is MastodonAttachmentService.UploadState.Finish {
|
||||
continue
|
||||
}
|
||||
if currentState is MastodonAttachmentService.UploadState.Uploading {
|
||||
break
|
||||
}
|
||||
// trigger uploading one by one
|
||||
if currentState is MastodonAttachmentService.UploadState.Initial {
|
||||
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
|
||||
break
|
||||
}
|
||||
snapshot.appendItems(pollItems, toSection: .poll)
|
||||
if pollAttributes.count < 4 {
|
||||
snapshot.appendItems([ComposeStatusItem.pollOptionAppendEntry], toSection: .poll)
|
||||
}
|
||||
snapshot.appendItems([ComposeStatusItem.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)], toSection: .poll)
|
||||
}
|
||||
|
||||
diffableDataSource.apply(snapshot)
|
||||
|
||||
// drive service upload state
|
||||
// make image upload in the queue
|
||||
for attachmentService in attachmentServices {
|
||||
// skip when prefix N task when task finish OR fail OR uploading
|
||||
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
|
||||
if currentState is MastodonAttachmentService.UploadState.Fail {
|
||||
continue
|
||||
}
|
||||
if currentState is MastodonAttachmentService.UploadState.Finish {
|
||||
continue
|
||||
}
|
||||
if currentState is MastodonAttachmentService.UploadState.Uploading {
|
||||
break
|
||||
}
|
||||
// trigger uploading one by one
|
||||
if currentState is MastodonAttachmentService.UploadState.Initial {
|
||||
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind delegate
|
||||
attachmentServices
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
attachmentServices.forEach { $0.delegate = self }
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
pollOptionAttributes
|
||||
.sink { [weak self] pollAttributes in
|
||||
guard let self = self else { return }
|
||||
pollAttributes.forEach { $0.delegate = self }
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind compose toolbar UI state
|
||||
Publishers.CombineLatest(
|
||||
isPollComposing.eraseToAnyPublisher(),
|
||||
attachmentServices.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
|
||||
guard let self = self else { return }
|
||||
let shouldMediaDisable = isPollComposing || attachmentServices.count >= 4
|
||||
let shouldPollDisable = attachmentServices.count > 0
|
||||
|
||||
self.isMediaToolbarButtonEnabled.value = !shouldMediaDisable
|
||||
self.isPollToolbarButtonEnabled.value = !shouldPollDisable
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
func createNewPollOptionIfPossible() {
|
||||
guard pollOptionAttributes.value.count < 4 else { return }
|
||||
|
||||
let attribute = ComposeStatusItem.ComposePollOptionAttribute()
|
||||
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MastodonAttachmentServiceDelegate
|
||||
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
|
||||
|
@ -187,3 +282,11 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
|||
attachmentServices.value = attachmentServices.value
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ComposePollAttributeDelegate
|
||||
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
||||
// trigger update
|
||||
pollOptionAttributes.value = pollOptionAttributes.value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
// Created by MainasuK Cirno on 2021-3-12.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
protocol ComposeToolbarViewDelegate: class {
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton)
|
||||
}
|
||||
|
||||
final class ComposeToolbarView: UIView {
|
||||
|
@ -102,10 +103,10 @@ extension ComposeToolbarView {
|
|||
|
||||
mediaButton.menu = createMediaContextMenu()
|
||||
mediaButton.showsMenuAsPrimaryAction = true
|
||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
|
||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
|
||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
|
||||
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.locationButtonDidPressed(_:)), for: .touchUpInside)
|
||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
|
||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
||||
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.visibilityButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,18 +132,21 @@ extension ComposeToolbarView {
|
|||
var children: [UIMenuElement] = []
|
||||
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary)
|
||||
}
|
||||
children.append(photoLibraryAction)
|
||||
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .camera)
|
||||
})
|
||||
children.append(cameraAction)
|
||||
}
|
||||
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .browse)
|
||||
}
|
||||
children.append(browseAction)
|
||||
|
@ -155,20 +159,24 @@ extension ComposeToolbarView {
|
|||
|
||||
extension ComposeToolbarView {
|
||||
|
||||
@objc private func gifButtonDidPressed(_ sender: UIButton) {
|
||||
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
|
||||
@objc private func pollButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.composeToolbarView(self, pollButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func atButtonDidPressed(_ sender: UIButton) {
|
||||
delegate?.composeToolbarView(self, atButtonDidPressed: sender)
|
||||
@objc private func emojiButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.composeToolbarView(self, emojiButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func topicButtonDidPressed(_ sender: UIButton) {
|
||||
delegate?.composeToolbarView(self, topicButtonDidPressed: sender)
|
||||
@objc private func contentWarningButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func locationButtonDidPressed(_ sender: UIButton) {
|
||||
delegate?.composeToolbarView(self, locationButtonDidPressed: sender)
|
||||
@objc private func visibilityButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.composeToolbarView(self, visibilityButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
//
|
||||
// PollOptionView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class PollOptionView: UIView {
|
||||
|
||||
static let height: CGFloat = optionHeight + 2 * verticalMargin
|
||||
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<AnyCancellable>()
|
||||
|
||||
let roundedBackgroundView = UIView()
|
||||
let voteProgressStripView: StripProgressView = {
|
||||
let view = StripProgressView()
|
||||
view.tintColor = Asset.Colors.Background.Poll.highlight.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let checkmarkBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBackground
|
||||
return view
|
||||
}()
|
||||
|
||||
let checkmarkImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
||||
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Button.normal.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let plusCircleImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let image = Asset.Circles.plusCircle.image
|
||||
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Button.normal.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let optionTextField: DeleteBackwardResponseTextField = {
|
||||
let textField = DeleteBackwardResponseTextField()
|
||||
textField.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.text = "Option"
|
||||
textField.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
|
||||
return textField
|
||||
}()
|
||||
|
||||
let optionLabelMiddlePaddingView = UIView()
|
||||
|
||||
let optionPercentageLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "50%"
|
||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PollOptionView {
|
||||
private func _init() {
|
||||
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(roundedBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
roundedBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||
roundedBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
|
||||
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionView.optionHeight).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(voteProgressStripView)
|
||||
NSLayoutConstraint.activate([
|
||||
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
|
||||
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
|
||||
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
|
||||
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
|
||||
])
|
||||
|
||||
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, 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),
|
||||
])
|
||||
|
||||
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
checkmarkBackgroundView.addSubview(checkmarkImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
|
||||
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
|
||||
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
|
||||
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
|
||||
])
|
||||
|
||||
plusCircleImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(plusCircleImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
plusCircleImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor),
|
||||
plusCircleImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor),
|
||||
plusCircleImageView.trailingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor),
|
||||
plusCircleImageView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor),
|
||||
])
|
||||
|
||||
optionTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionTextField)
|
||||
NSLayoutConstraint.activate([
|
||||
optionTextField.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
|
||||
optionTextField.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
optionTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionTextField.trailingAnchor),
|
||||
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
|
||||
])
|
||||
optionLabelMiddlePaddingView.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
optionLabelMiddlePaddingView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionPercentageLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
||||
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
])
|
||||
optionPercentageLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
|
||||
plusCircleImageView.isHidden = true
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateCornerRadius()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PollOptionView {
|
||||
private func updateCornerRadius() {
|
||||
roundedBackgroundView.layer.masksToBounds = true
|
||||
roundedBackgroundView.layer.cornerRadius = PollOptionView.optionHeight * 0.5
|
||||
roundedBackgroundView.layer.cornerCurve = .circular
|
||||
|
||||
checkmarkBackgroundView.layer.masksToBounds = true
|
||||
checkmarkBackgroundView.layer.cornerRadius = PollOptionView.checkmarkImageSize.width * 0.5
|
||||
checkmarkBackgroundView.layer.cornerCurve = .circular
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct PollOptionView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
PollOptionView()
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 100))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -10,54 +10,8 @@ import Combine
|
|||
|
||||
final class PollOptionTableViewCell: UITableViewCell {
|
||||
|
||||
static let height: CGFloat = optionHeight + 2 * verticalMargin
|
||||
static let optionHeight: CGFloat = 44
|
||||
static let verticalMargin: CGFloat = 5
|
||||
static let checkmarkImageSize = CGSize(width: 26, height: 26)
|
||||
|
||||
private var viewStateDisposeBag = Set<AnyCancellable>()
|
||||
let pollOptionView = PollOptionView()
|
||||
var attribute: PollItem.Attribute?
|
||||
|
||||
let roundedBackgroundView = UIView()
|
||||
let voteProgressStripView: StripProgressView = {
|
||||
let view = StripProgressView()
|
||||
view.tintColor = Asset.Colors.Background.Poll.highlight.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let checkmarkBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBackground
|
||||
return view
|
||||
}()
|
||||
|
||||
let checkmarkImageView: UIView = {
|
||||
let imageView = UIImageView()
|
||||
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
||||
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Button.normal.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let optionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "Option"
|
||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
|
||||
return label
|
||||
}()
|
||||
|
||||
let optionLabelMiddlePaddingView = UIView()
|
||||
|
||||
let optionPercentageLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "50%"
|
||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
@ -76,7 +30,7 @@ final class PollOptionTableViewCell: UITableViewCell {
|
|||
switch voteState {
|
||||
case .hidden:
|
||||
let color = Asset.Colors.Background.systemGroupedBackground.color
|
||||
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||
case .reveal:
|
||||
break
|
||||
}
|
||||
|
@ -89,7 +43,7 @@ final class PollOptionTableViewCell: UITableViewCell {
|
|||
switch voteState {
|
||||
case .hidden:
|
||||
let color = Asset.Colors.Background.systemGroupedBackground.color
|
||||
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||
case .reveal:
|
||||
break
|
||||
}
|
||||
|
@ -102,125 +56,55 @@ extension PollOptionTableViewCell {
|
|||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
pollOptionView.optionTextField.isUserInteractionEnabled = false
|
||||
|
||||
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(roundedBackgroundView)
|
||||
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(pollOptionView)
|
||||
NSLayoutConstraint.activate([
|
||||
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
|
||||
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
|
||||
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
|
||||
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
pollOptionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
pollOptionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(voteProgressStripView)
|
||||
NSLayoutConstraint.activate([
|
||||
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
|
||||
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
|
||||
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
|
||||
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
|
||||
])
|
||||
|
||||
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
|
||||
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9),
|
||||
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
|
||||
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.width).priority(.defaultHigh),
|
||||
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.height).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
checkmarkBackgroundView.addSubview(checkmarkImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
|
||||
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
|
||||
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
|
||||
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
|
||||
])
|
||||
|
||||
optionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
optionLabel.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
|
||||
optionLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
])
|
||||
|
||||
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionLabel.trailingAnchor),
|
||||
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
|
||||
])
|
||||
optionLabelMiddlePaddingView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
|
||||
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionPercentageLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
||||
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
])
|
||||
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
updateCornerRadius()
|
||||
updateTextAppearance()
|
||||
}
|
||||
|
||||
private func updateCornerRadius() {
|
||||
roundedBackgroundView.layer.masksToBounds = true
|
||||
roundedBackgroundView.layer.cornerRadius = PollOptionTableViewCell.optionHeight * 0.5
|
||||
roundedBackgroundView.layer.cornerCurve = .circular
|
||||
|
||||
checkmarkBackgroundView.layer.masksToBounds = true
|
||||
checkmarkBackgroundView.layer.cornerRadius = PollOptionTableViewCell.checkmarkImageSize.width * 0.5
|
||||
checkmarkBackgroundView.layer.cornerCurve = .circular
|
||||
}
|
||||
|
||||
|
||||
func updateTextAppearance() {
|
||||
guard let voteState = attribute?.voteState else {
|
||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||
optionLabel.layer.removeShadow()
|
||||
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
|
||||
pollOptionView.optionTextField.layer.removeShadow()
|
||||
return
|
||||
}
|
||||
|
||||
switch voteState {
|
||||
case .hidden:
|
||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||
optionLabel.layer.removeShadow()
|
||||
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
|
||||
pollOptionView.optionTextField.layer.removeShadow()
|
||||
case .reveal(_, let percentage, _):
|
||||
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.minX {
|
||||
optionLabel.textColor = .white
|
||||
optionLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||
if CGFloat(percentage) * pollOptionView.voteProgressStripView.frame.width > pollOptionView.optionLabelMiddlePaddingView.frame.minX {
|
||||
pollOptionView.optionTextField.textColor = .white
|
||||
pollOptionView.optionTextField.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||
} else {
|
||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||
optionLabel.layer.removeShadow()
|
||||
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
|
||||
pollOptionView.optionTextField.layer.removeShadow()
|
||||
}
|
||||
|
||||
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.maxX {
|
||||
optionPercentageLabel.textColor = .white
|
||||
optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||
if CGFloat(percentage) * pollOptionView.voteProgressStripView.frame.width > pollOptionView.optionLabelMiddlePaddingView.frame.maxX {
|
||||
pollOptionView.optionPercentageLabel.textColor = .white
|
||||
pollOptionView.optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||
} else {
|
||||
optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
|
||||
optionPercentageLabel.layer.removeShadow()
|
||||
pollOptionView.optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
|
||||
pollOptionView.optionPercentageLabel.layer.removeShadow()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateTextAppearance()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// DeleteBackwardResponseTextField.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol DeleteBackwardResponseTextFieldDelegate: class {
|
||||
func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?)
|
||||
}
|
||||
|
||||
final class DeleteBackwardResponseTextField: UITextField {
|
||||
|
||||
weak var deleteBackwardDelegate: DeleteBackwardResponseTextFieldDelegate?
|
||||
|
||||
override func deleteBackward() {
|
||||
let text = self.text
|
||||
super.deleteBackward()
|
||||
deleteBackwardDelegate?.deleteBackwardResponseTextField(self, textBeforeDelete: text)
|
||||
}
|
||||
|
||||
}
|
|
@ -96,15 +96,34 @@ extension Mastodon.API.Statuses {
|
|||
public struct PublishStatusQuery: Codable, PostQuery {
|
||||
public let status: String?
|
||||
public let mediaIDs: [String]?
|
||||
public let pollOptions: [String]?
|
||||
public let pollExpiresIn: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case mediaIDs = "media_ids"
|
||||
}
|
||||
|
||||
public init(status: String?, mediaIDs: [String]?) {
|
||||
public init(status: String?, mediaIDs: [String]?, pollOptions: [String]?, pollExpiresIn: Int?) {
|
||||
self.status = status
|
||||
self.mediaIDs = mediaIDs
|
||||
self.pollOptions = pollOptions
|
||||
self.pollExpiresIn = pollExpiresIn
|
||||
}
|
||||
|
||||
var contentType: String? {
|
||||
return Self.multipartContentType()
|
||||
}
|
||||
|
||||
var body: Data? {
|
||||
var data = Data()
|
||||
|
||||
status.flatMap { data.append(Data.multipart(key: "status", value: $0)) }
|
||||
for mediaID in mediaIDs ?? [] {
|
||||
data.append(Data.multipart(key: "media_ids[]", value: mediaID))
|
||||
}
|
||||
for pollOption in pollOptions ?? [] {
|
||||
data.append(Data.multipart(key: "poll[options][]", value: pollOption))
|
||||
}
|
||||
pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
|
||||
|
||||
data.append(Data.multipartEnd())
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,3 +35,12 @@ extension String: MultipartFormValue {
|
|||
var multipartContentType: String? { return nil }
|
||||
var multipartFilename: String? { return nil }
|
||||
}
|
||||
|
||||
|
||||
extension Int: MultipartFormValue {
|
||||
var multipartValue: Data {
|
||||
return String(self).data(using: .utf8)!
|
||||
}
|
||||
var multipartContentType: String? { return nil }
|
||||
var multipartFilename: String? { return nil }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue