feat: complete compose scene refactor
This commit is contained in:
parent
2d374f5908
commit
b435857214
|
@ -226,6 +226,12 @@
|
|||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
||||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
||||
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
||||
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; };
|
||||
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; };
|
||||
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; };
|
||||
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */; };
|
||||
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */; };
|
||||
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.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 */; };
|
||||
|
@ -344,8 +350,6 @@
|
|||
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; };
|
||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */; };
|
||||
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; };
|
||||
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
|
||||
|
@ -842,6 +846,13 @@
|
|||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRelationshipActionButton.swift; sourceTree = "<group>"; };
|
||||
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = "<group>"; };
|
||||
DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
|
||||
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = "<group>"; };
|
||||
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = "<group>"; };
|
||||
DB3667A2268AC3BB0027D07F /* MetaTextView */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MetaTextView; path = ../MetaTextView; sourceTree = "<group>"; };
|
||||
DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = "<group>"; };
|
||||
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.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; };
|
||||
|
@ -957,8 +968,6 @@
|
|||
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = "<group>"; };
|
||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.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>"; };
|
||||
|
@ -1529,6 +1538,8 @@
|
|||
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
|
||||
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||
|
@ -1593,6 +1604,8 @@
|
|||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */,
|
||||
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */,
|
||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
||||
|
@ -1747,6 +1760,8 @@
|
|||
children = (
|
||||
DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */,
|
||||
DB03F7EF26899097007B274C /* ComposeStatusContentTableViewCell.swift */,
|
||||
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||
DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */,
|
||||
);
|
||||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1812,6 +1827,7 @@
|
|||
children = (
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */,
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */,
|
||||
DB3667A2268AC3BB0027D07F /* MetaTextView */,
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
|
||||
DB427DD425BAA00100D1B89D /* Mastodon */,
|
||||
DB427DEB25BAA00100D1B89D /* MastodonTests */,
|
||||
|
@ -2114,8 +2130,6 @@
|
|||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
|
||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */,
|
||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||
|
@ -3217,6 +3231,7 @@
|
|||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
|
||||
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */,
|
||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
||||
|
@ -3240,6 +3255,7 @@
|
|||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */,
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
|
||||
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */,
|
||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
|
||||
DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */,
|
||||
|
@ -3329,17 +3345,18 @@
|
|||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */,
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */,
|
||||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */,
|
||||
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
||||
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
||||
|
@ -3426,6 +3443,7 @@
|
|||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||
|
@ -3494,6 +3512,7 @@
|
|||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */,
|
||||
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */,
|
||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||
DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */,
|
||||
|
@ -3516,7 +3535,6 @@
|
|||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
|
||||
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
||||
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
||||
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
||||
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>21</integer>
|
||||
<integer>23</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>22</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// ComposeStatusAttachmentItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ComposeStatusAttachmentItem {
|
||||
case attachment(attachmentService: MastodonAttachmentService)
|
||||
}
|
||||
|
||||
extension ComposeStatusAttachmentItem: Hashable { }
|
|
@ -13,14 +13,10 @@ import CoreData
|
|||
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)
|
||||
case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute)
|
||||
case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute)
|
||||
}
|
||||
|
||||
extension ComposeStatusItem: Equatable { }
|
||||
|
||||
extension ComposeStatusItem: Hashable { }
|
||||
|
||||
extension ComposeStatusItem {
|
||||
|
@ -50,88 +46,22 @@ extension ComposeStatusItem {
|
|||
}
|
||||
}
|
||||
|
||||
protocol ComposePollAttributeDelegate: AnyObject {
|
||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?)
|
||||
}
|
||||
|
||||
extension ComposeStatusItem {
|
||||
final class ComposePollOptionAttribute: Equatable, Hashable {
|
||||
final class ComposeStatusAttachmentAttribute: 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)
|
||||
var attachmentServices: [MastodonAttachmentService]
|
||||
|
||||
init(attachmentServices: [MastodonAttachmentService]) {
|
||||
self.attachmentServices = attachmentServices
|
||||
}
|
||||
|
||||
deinit {
|
||||
disposeBag.removeAll()
|
||||
|
||||
static func == (lhs: ComposeStatusAttachmentAttribute, rhs: ComposeStatusAttachmentAttribute) -> Bool {
|
||||
return lhs.attachmentServices == rhs.attachmentServices
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// ComposeStatusPollItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
enum ComposeStatusPollItem {
|
||||
case pollOption(attribute: PollOptionAttribute)
|
||||
case pollOptionAppendEntry
|
||||
case pollExpiresOption(attribute: PollExpiresOptionAttribute)
|
||||
}
|
||||
|
||||
extension ComposeStatusPollItem: Hashable { }
|
||||
|
||||
extension ComposeStatusPollItem {
|
||||
|
||||
final class PollOptionAttribute: 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: PollOptionAttribute, rhs: PollOptionAttribute) -> Bool {
|
||||
return lhs.id == rhs.id &&
|
||||
lhs.option.value == rhs.option.value
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protocol ComposePollAttributeDelegate: AnyObject {
|
||||
func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?)
|
||||
}
|
||||
|
||||
extension ComposeStatusPollItem {
|
||||
final class PollExpiresOptionAttribute: Equatable, Hashable {
|
||||
private let id = UUID()
|
||||
|
||||
let expiresOption = CurrentValueSubject<ExpiresOption, Never>(.thirtyMinutes)
|
||||
|
||||
|
||||
static func == (lhs: PollExpiresOptionAttribute, rhs: PollExpiresOptionAttribute) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// ComposeStatusAttachmentSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ComposeStatusAttachmentSection: Hashable {
|
||||
case main
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// ComposeStatusPollSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ComposeStatusPollSection: Hashable {
|
||||
case main
|
||||
}
|
|
@ -30,266 +30,6 @@ extension ComposeStatusSection {
|
|||
}
|
||||
|
||||
extension ComposeStatusSection {
|
||||
static func collectionViewDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
composeKind: ComposeKind,
|
||||
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||
metaTextDelegate: MetaTextDelegate,
|
||||
metaTextViewDelegate: UITextViewDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||
weak customEmojiPickerInputViewModel,
|
||||
weak metaTextDelegate,
|
||||
weak metaTextViewDelegate,
|
||||
weak composeStatusAttachmentTableViewCellDelegate,
|
||||
weak composeStatusPollOptionCollectionViewCellDelegate,
|
||||
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||
weak composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
switch item {
|
||||
case .replyTo(let replyToStatusObjectID):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
|
||||
// set empty text before retrieve real data to fix pseudo-text display issue
|
||||
cell.statusView.nameLabel.text = " "
|
||||
cell.statusView.usernameLabel.text = " "
|
||||
managedObjectContext.performAndWait {
|
||||
guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||
return
|
||||
}
|
||||
let status = replyTo.reblog ?? replyTo
|
||||
|
||||
// set avatar
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||
// set name username
|
||||
cell.statusView.nameLabel.text = {
|
||||
let author = status.author
|
||||
return author.displayName.isEmpty ? author.username : author.displayName
|
||||
}()
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
// set text
|
||||
//status.emoji
|
||||
// cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:])
|
||||
// set date
|
||||
cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow
|
||||
|
||||
cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .input(let replyToStatusObjectID, let attribute):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(
|
||||
document: MastodonContent(content: attribute.composeContent.value ?? "", emojis: [:])
|
||||
)
|
||||
cell.metaText.configure(content: metaContent)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
}
|
||||
cell.metaText.delegate = metaTextDelegate
|
||||
cell.metaText.textView.delegate = metaTextViewDelegate
|
||||
cell.statusContentWarningEditorView.textView.text = attribute.contentWarningContent.value
|
||||
managedObjectContext.performAndWait {
|
||||
guard let replyToStatusObjectID = replyToStatusObjectID,
|
||||
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
return
|
||||
}
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback)
|
||||
}
|
||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
||||
// cell.composeContent
|
||||
// .removeDuplicates()
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak collectionView] text in
|
||||
// guard let collectionView = collectionView else { return }
|
||||
// // self size input cell
|
||||
// // needs restore content offset to resolve issue #83
|
||||
// let oldContentOffset = collectionView.contentOffset
|
||||
// collectionView.collectionViewLayout.invalidateLayout()
|
||||
// collectionView.layoutIfNeeded()
|
||||
// collectionView.contentOffset = oldContentOffset
|
||||
//
|
||||
// // bind input data
|
||||
// attribute.composeContent.value = text
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
attribute.isContentWarningComposing
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell, weak collectionView] isContentWarningComposing in
|
||||
guard let cell = cell else { return }
|
||||
guard let collectionView = collectionView else { return }
|
||||
// self size input cell
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
||||
cell.statusContentWarningEditorView.alpha = 0
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||
cell.statusContentWarningEditorView.alpha = 1
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.contentWarningContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak collectionView] text in
|
||||
guard let collectionView = collectionView else { return }
|
||||
// self size input cell
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
// bind input data
|
||||
attribute.contentWarningContent.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
||||
|
||||
return cell
|
||||
case .attachment(let attachmentService):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
||||
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
|
||||
cell.delegate = composeStatusAttachmentTableViewCellDelegate
|
||||
attachmentService.thumbnailImage
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] thumbnailImage in
|
||||
guard let cell = cell else { return }
|
||||
let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
|
||||
guard let image = thumbnailImage else {
|
||||
let placeholder = UIImage.placeholder(
|
||||
size: size,
|
||||
color: Asset.Colors.Background.systemGroupedBackground.color
|
||||
)
|
||||
.af.imageRounded(
|
||||
withCornerRadius: AttachmentContainerView.containerViewCornerRadius
|
||||
)
|
||||
cell.attachmentContainerView.previewImageView.image = placeholder
|
||||
return
|
||||
}
|
||||
cell.attachmentContainerView.previewImageView.image = image
|
||||
.af.imageAspectScaled(toFill: size)
|
||||
.af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
Publishers.CombineLatest(
|
||||
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
|
||||
attachmentService.error.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell, weak attachmentService] uploadState, error in
|
||||
guard let cell = cell else { return }
|
||||
guard let attachmentService = attachmentService else { return }
|
||||
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
|
||||
cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
|
||||
if let error = error {
|
||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||
cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
|
||||
} else {
|
||||
guard let uploadState = uploadState else { return }
|
||||
switch uploadState {
|
||||
case is MastodonAttachmentService.UploadState.Finish,
|
||||
is MastodonAttachmentService.UploadState.Fail:
|
||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||
cell.attachmentContainerView.emptyStateView.label.text = {
|
||||
if let file = attachmentService.file.value {
|
||||
switch file {
|
||||
case .jpeg, .png, .gif:
|
||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||
case .other:
|
||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
||||
}
|
||||
} else {
|
||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
NotificationCenter.default.publisher(
|
||||
for: UITextView.textDidChangeNotification,
|
||||
object: cell.attachmentContainerView.descriptionTextView
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { notification in
|
||||
guard let textField = notification.object as? UITextView else { return }
|
||||
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
attachmentService.description.value = text
|
||||
}
|
||||
.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.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
|
||||
cell.pollOption
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: attribute.option)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
|
||||
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 { [weak cell] expiresOption in
|
||||
guard let cell = cell else { return }
|
||||
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.delegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeStatusSection {
|
||||
|
||||
static func configureStatusContent(
|
||||
cell: ComposeStatusContentCollectionViewCell,
|
||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||
) {
|
||||
// set avatar
|
||||
attribute.avatarURL
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { avatarURL in
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
// set display name and username
|
||||
Publishers.CombineLatest(
|
||||
attribute.displayName.eraseToAnyPublisher(),
|
||||
attribute.username.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { displayName, username in
|
||||
cell.statusView.nameLabel.text = displayName
|
||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// bind compose content
|
||||
cell.composeContent
|
||||
.map { $0 as String? }
|
||||
.assign(to: \.value, on: attribute.composeContent)
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
static func configureStatusContent(
|
||||
cell: ComposeStatusContentTableViewCell,
|
||||
|
@ -335,16 +75,6 @@ class CustomEmojiReplaceableTextInputReference {
|
|||
}
|
||||
}
|
||||
|
||||
//extension TextEditorView: CustomEmojiReplaceableTextInput {
|
||||
// func insertText(_ text: String) {
|
||||
// try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil)
|
||||
// }
|
||||
//
|
||||
// public override var isFirstResponder: Bool {
|
||||
// return isEditing
|
||||
// }
|
||||
//
|
||||
//}
|
||||
extension UITextField: CustomEmojiReplaceableTextInput { }
|
||||
extension UITextView: CustomEmojiReplaceableTextInput { }
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
import Nuke
|
||||
|
||||
enum CustomEmojiPickerSection: Equatable, Hashable {
|
||||
case emoji(name: String)
|
||||
|
@ -24,13 +24,13 @@ extension CustomEmojiPickerSection {
|
|||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
|
||||
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
|
||||
.af.imageRounded(withCornerRadius: 4)
|
||||
cell.emojiImageView.kf.setImage(
|
||||
with: URL(string: attribute.emoji.url),
|
||||
placeholder: placeholder,
|
||||
options: [
|
||||
.transition(.fade(0.2))
|
||||
],
|
||||
completionHandler: nil
|
||||
cell.imageTask = Nuke.loadImage(
|
||||
with: attribute.emoji.url,
|
||||
options: .init(
|
||||
placeholder: placeholder,
|
||||
transition: .fadeIn(duration: 0.2)
|
||||
),
|
||||
into: cell.emojiImageView
|
||||
)
|
||||
cell.accessibilityLabel = attribute.emoji.shortcode
|
||||
return cell
|
||||
|
@ -48,7 +48,7 @@ extension CustomEmojiPickerSection {
|
|||
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
|
||||
switch section {
|
||||
case .emoji(let name):
|
||||
header.titlelabel.text = name
|
||||
header.titleLabel.text = name
|
||||
}
|
||||
return header
|
||||
default:
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
//
|
||||
// ComposeRepliedToStatusContentCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-11.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let statusView = StatusView()
|
||||
|
||||
let framePublisher = PassthroughSubject<CGRect, Never>()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
framePublisher.send(bounds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeRepliedToStatusContentCollectionViewCell {
|
||||
|
||||
private func _init() {
|
||||
backgroundColor = .clear
|
||||
|
||||
statusView.actionToolbarContainer.isHidden = true
|
||||
statusView.revealContentWarningButton.isHidden = true
|
||||
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).identifier("statusView.top to ComposeRepliedToStatusContentCollectionViewCell.contentView.top"),
|
||||
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10).identifier("ComposeRepliedToStatusContentCollectionViewCell.contentView.bottom to statusView.bottom"),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
//
|
||||
// ComposeStatusContentCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-11.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MetaTextView
|
||||
|
||||
final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let statusView = StatusView()
|
||||
|
||||
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
||||
|
||||
let textEditorViewContainerView = UIView()
|
||||
|
||||
static let metaTextViewTag: Int = 333
|
||||
let metaText: MetaText = {
|
||||
let metaText = MetaText()
|
||||
metaText.textView.tag = ComposeStatusContentCollectionViewCell.metaTextViewTag
|
||||
metaText.textView.isScrollEnabled = false
|
||||
metaText.textView.keyboardType = .twitter
|
||||
metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
metaText.textView.attributedPlaceholder = {
|
||||
var attributes = metaText.textAttributes
|
||||
attributes[.foregroundColor] = Asset.Colors.Label.secondary.color
|
||||
return NSAttributedString(
|
||||
string: L10n.Scene.Compose.contentInputPlaceholder,
|
||||
attributes: attributes
|
||||
)
|
||||
}()
|
||||
return metaText
|
||||
}()
|
||||
|
||||
// output
|
||||
let composeContent = PassthroughSubject<String, Never>()
|
||||
let contentWarningContent = PassthroughSubject<String, Never>()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
metaText.delegate = nil
|
||||
metaText.textView.delegate = nil
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusContentCollectionViewCell {
|
||||
|
||||
private func _init() {
|
||||
// selectionStyle = .none
|
||||
layer.zPosition = 999
|
||||
preservesSuperviewLayoutMargins = true
|
||||
|
||||
statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusContentWarningEditorView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusContentWarningEditorView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
statusContentWarningEditorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
statusContentWarningEditorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
])
|
||||
statusContentWarningEditorView.preservesSuperviewLayoutMargins = true
|
||||
statusContentWarningEditorView.containerBackgroundView.isHidden = false
|
||||
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.topAnchor.constraint(equalTo: statusContentWarningEditorView.bottomAnchor, constant: 20),
|
||||
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
])
|
||||
statusView.statusContainerStackView.isHidden = true
|
||||
statusView.actionToolbarContainer.isHidden = true
|
||||
statusView.nameTrialingDotLabel.isHidden = true
|
||||
statusView.dateLabel.isHidden = true
|
||||
|
||||
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
|
||||
textEditorViewContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(textEditorViewContainerView)
|
||||
NSLayoutConstraint.activate([
|
||||
textEditorViewContainerView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||
textEditorViewContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
textEditorViewContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor, constant: 10),
|
||||
])
|
||||
textEditorViewContainerView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
// textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// textEditorViewContainerView.addSubview(textEditorView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// textEditorView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor),
|
||||
// textEditorView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.leadingAnchor),
|
||||
// textEditorView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.trailingAnchor),
|
||||
// textEditorView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor),
|
||||
// textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
// ])
|
||||
// textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
|
||||
metaText.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
textEditorViewContainerView.addSubview(metaText.textView)
|
||||
NSLayoutConstraint.activate([
|
||||
metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor),
|
||||
metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.leadingAnchor),
|
||||
metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.trailingAnchor),
|
||||
metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor),
|
||||
metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 88).priority(.defaultHigh),
|
||||
])
|
||||
metaText.textView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
|
||||
statusContentWarningEditorView.textView.delegate = self
|
||||
//textEditorView.changeObserver = self
|
||||
|
||||
statusContentWarningEditorView.isHidden = true
|
||||
statusView.revealContentWarningButton.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - TextEditorViewChangeObserver
|
||||
//extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
||||
// func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||
// defer {
|
||||
// textEditorViewChangeObserver?.textEditorView(textEditorView, didChangeWithChangeResult: changeResult)
|
||||
// }
|
||||
//
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
|
||||
// guard changeResult.isTextChanged else { return }
|
||||
// composeContent.send(textEditorView.text)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - UITextViewDelegate
|
||||
extension ComposeStatusContentCollectionViewCell: UITextViewDelegate {
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if textView === statusContentWarningEditorView.textView {
|
||||
// disable input line break
|
||||
guard text != "\n" else { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textView.text)
|
||||
guard textView === statusContentWarningEditorView.textView else { return }
|
||||
// replace line break with space
|
||||
textView.text = textView.text.replacingOccurrences(of: "\n", with: " ")
|
||||
contentWarningContent.send(textView.text)
|
||||
}
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
|
||||
protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject {
|
||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption)
|
||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption)
|
||||
}
|
||||
|
||||
final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
|
||||
|
@ -41,7 +41,7 @@ final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCe
|
|||
|
||||
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||
|
||||
private typealias ExpiresOption = ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption
|
||||
private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption
|
||||
|
||||
private func _init() {
|
||||
durationButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -9,7 +9,7 @@ import UIKit
|
|||
|
||||
final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableView {
|
||||
|
||||
let titlelabel: UILabel = {
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .bold))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
|
@ -30,13 +30,13 @@ final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableV
|
|||
|
||||
extension CustomEmojiPickerHeaderCollectionReusableView {
|
||||
private func _init() {
|
||||
titlelabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(titlelabel)
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(titleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
titlelabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
||||
titlelabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
titlelabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
titlelabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
||||
titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Nuke
|
||||
|
||||
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
static let itemSize = CGSize(width: 44, height: 44)
|
||||
|
||||
var imageTask: ImageTask?
|
||||
|
||||
let emojiImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
|
@ -23,6 +26,12 @@ final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
|
|||
emojiImageView.alpha = isHighlighted ? 0.5 : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
imageTask?.cancel()
|
||||
imageTask = nil
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
|
@ -48,26 +48,12 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
// let collectionView: ComposeCollectionView = {
|
||||
// let collectionViewLayout = ComposeViewController.createLayout()
|
||||
// let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||
// collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.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.Scene.Compose.background.color
|
||||
// collectionView.alwaysBounceVertical = true
|
||||
// collectionView.keyboardDismissMode = .onDrag
|
||||
// return collectionView
|
||||
// }()
|
||||
|
||||
let tableView: ComposeTableView = {
|
||||
let tableView = ComposeTableView()
|
||||
tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self))
|
||||
tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self))
|
||||
tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
|
||||
tableView.backgroundColor = Asset.Scene.Compose.background.color
|
||||
tableView.alwaysBounceVertical = true
|
||||
tableView.separatorStyle = .none
|
||||
|
@ -174,15 +160,6 @@ extension ComposeViewController {
|
|||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
// collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.addSubview(collectionView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
// collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
// collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
// ])
|
||||
|
||||
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(composeToolbarView)
|
||||
|
@ -210,7 +187,11 @@ extension ComposeViewController {
|
|||
tableView: tableView,
|
||||
metaTextDelegate: self,
|
||||
metaTextViewDelegate: self,
|
||||
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel
|
||||
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
||||
composeStatusAttachmentCollectionViewCellDelegate: self,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: self
|
||||
)
|
||||
|
||||
viewModel.composeStatusAttribute.composeContent
|
||||
|
@ -218,6 +199,7 @@ extension ComposeViewController {
|
|||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard self.view.window != nil else { return }
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.beginUpdates()
|
||||
self.tableView.endUpdates()
|
||||
|
@ -225,21 +207,6 @@ extension ComposeViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// collectionView.delegate = self
|
||||
// viewModel.setupDiffableDataSource(
|
||||
// for: collectionView,
|
||||
// dependency: self,
|
||||
// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
||||
// metaTextDelegate: self,
|
||||
// metaTextViewDelegate: self,
|
||||
// composeStatusAttachmentTableViewCellDelegate: self,
|
||||
// composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||
// composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
||||
// composeStatusPollExpiresOptionCollectionViewCellDelegate: self
|
||||
// )
|
||||
// let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
||||
// collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||
|
||||
customEmojiPickerInputView.collectionView.delegate = self
|
||||
viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
|
||||
viewModel.setupCustomEmojiPickerDiffableDataSource(
|
||||
|
@ -273,8 +240,8 @@ extension ComposeViewController {
|
|||
// update keyboard background color
|
||||
|
||||
guard isShow, state == .dock else {
|
||||
self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||
self.tableView.contentInset.bottom = extraMargin
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin
|
||||
|
||||
if let superView = self.autoCompleteViewController.tableView.superview {
|
||||
let autoCompleteTableViewBottomInset: CGFloat = {
|
||||
|
@ -319,8 +286,8 @@ extension ComposeViewController {
|
|||
return
|
||||
}
|
||||
|
||||
self.tableView.contentInset.bottom = padding
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = padding
|
||||
self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
|
||||
self.view.layoutIfNeeded()
|
||||
|
@ -487,6 +454,12 @@ extension ComposeViewController {
|
|||
self.markTextEditorViewBecomeFirstResponser()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.isViewAppeared = true
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
@ -526,56 +499,56 @@ extension ComposeViewController {
|
|||
viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView
|
||||
}
|
||||
|
||||
// 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 pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard case .pollOption = item else { return nil }
|
||||
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
|
||||
guard let indexPath = dataSource.indexPath(for: item),
|
||||
let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
|
||||
// guard let cell = lastPollOptionCollectionViewCell() else { return }
|
||||
// cell.pollOptionView.optionTextField.becomeFirstResponder()
|
||||
// }
|
||||
return cell
|
||||
}
|
||||
|
||||
private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
|
||||
let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
|
||||
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 dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
|
||||
let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
|
||||
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(
|
||||
|
@ -652,43 +625,6 @@ extension ComposeViewController {
|
|||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// seealso: ComposeViewModel.setupDiffableDataSource(…)
|
||||
// @objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
||||
// switch(sender.state) {
|
||||
// case .began:
|
||||
// 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)),
|
||||
// let diffableDataSource = viewModel.diffableDataSource else {
|
||||
// break
|
||||
// }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
||||
// case .pollOption = item else {
|
||||
// collectionView.cancelInteractiveMovement()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var position = sender.location(in: collectionView)
|
||||
// position.x = collectionView.frame.width * 0.5
|
||||
// collectionView.updateInteractiveMovementTargetPosition(position)
|
||||
// case .ended:
|
||||
// collectionView.endInteractiveMovement()
|
||||
// collectionView.reloadData()
|
||||
// default:
|
||||
// collectionView.cancelInteractiveMovement()
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MetaTextDelegate
|
||||
|
@ -708,7 +644,7 @@ extension ComposeViewController: MetaTextDelegate {
|
|||
extension ComposeViewController: UITextViewDelegate {
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
if textView.tag == ComposeStatusContentCollectionViewCell.metaTextViewTag {
|
||||
if textEditorView()?.textView === textView {
|
||||
// update model
|
||||
guard let metaText = textEditorView() else { return }
|
||||
let backedString = metaText.backedString
|
||||
|
@ -857,7 +793,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
}
|
||||
self.suffixedAttachmentViews.removeAll()
|
||||
|
||||
// set normal apperance
|
||||
// set normal appearance
|
||||
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
|
||||
attributedString.removeAttribute(.underlineStyle, range: stringRange)
|
||||
|
@ -987,17 +923,17 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
|||
|
||||
// setup initial poll option if needs
|
||||
if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty {
|
||||
viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()]
|
||||
viewModel.pollOptionAttributes.value = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()]
|
||||
}
|
||||
|
||||
// if viewModel.isPollComposing.value {
|
||||
// // Magic RunLoop
|
||||
// DispatchQueue.main.async {
|
||||
// self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
|
||||
// }
|
||||
// } else {
|
||||
// markTextEditorViewBecomeFirstResponser()
|
||||
// }
|
||||
if viewModel.isPollComposing.value {
|
||||
// Magic RunLoop
|
||||
DispatchQueue.main.async {
|
||||
self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
|
||||
}
|
||||
} else {
|
||||
markTextEditorViewBecomeFirstResponser()
|
||||
}
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) {
|
||||
|
@ -1160,19 +1096,19 @@ extension ComposeViewController: UIDocumentPickerDelegate {
|
|||
extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
|
||||
|
||||
func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
|
||||
// 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 .attachment(attachmentService) = item else { return }
|
||||
//
|
||||
// var attachmentServices = viewModel.attachmentServices.value
|
||||
// guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
|
||||
// let removedItem = attachmentServices[index]
|
||||
// attachmentServices.remove(at: index)
|
||||
// viewModel.attachmentServices.value = attachmentServices
|
||||
//
|
||||
// // cancel task
|
||||
// removedItem.disposeBag.removeAll()
|
||||
guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return }
|
||||
guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
guard case let .attachment(attachmentService) = item else { return }
|
||||
|
||||
var attachmentServices = viewModel.attachmentServices.value
|
||||
guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
|
||||
let removedItem = attachmentServices[index]
|
||||
attachmentServices.remove(at: index)
|
||||
viewModel.attachmentServices.value = attachmentServices
|
||||
|
||||
// cancel task
|
||||
removedItem.disposeBag.removeAll()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1190,72 +1126,72 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega
|
|||
|
||||
// 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
|
||||
guard (text ?? "").isEmpty else { return }
|
||||
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
|
||||
guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
|
||||
guard let item = dataSource.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 = dataSource.snapshot().itemIdentifiers(inSection: .main)
|
||||
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()
|
||||
// }
|
||||
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
|
||||
guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
|
||||
let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in
|
||||
guard case .pollOption = item else { return false }
|
||||
return true
|
||||
}
|
||||
guard let item = dataSource.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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1264,15 +1200,15 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega
|
|||
extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate {
|
||||
func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) {
|
||||
viewModel.createNewPollOptionIfPossible()
|
||||
// DispatchQueue.main.async {
|
||||
// self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
|
||||
// }
|
||||
DispatchQueue.main.async {
|
||||
self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
|
||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) {
|
||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) {
|
||||
viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by MainasuK Cirno on 2021-3-11.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
@ -19,185 +20,83 @@ extension ComposeViewModel {
|
|||
tableView: UITableView,
|
||||
metaTextDelegate: MetaTextDelegate,
|
||||
metaTextViewDelegate: UITextViewDelegate,
|
||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel
|
||||
) {
|
||||
let dataSource = UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>(tableView: tableView) { [
|
||||
weak self,
|
||||
weak metaTextDelegate,
|
||||
weak metaTextViewDelegate,
|
||||
weak customEmojiPickerInputViewModel
|
||||
] tableView, indexPath, item in
|
||||
guard let self = self else { return UITableViewCell() }
|
||||
let managedObjectContext = self.context.managedObjectContext
|
||||
|
||||
switch item {
|
||||
case .replyTo(let statusObjectID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
guard let replyTo = managedObjectContext.object(with: statusObjectID) as? Status else {
|
||||
return
|
||||
}
|
||||
let status = replyTo.reblog ?? replyTo
|
||||
|
||||
// set avatar
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||
// set name username
|
||||
cell.statusView.nameLabel.text = {
|
||||
let author = status.author
|
||||
return author.displayName.isEmpty ? author.username : author.displayName
|
||||
}()
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
// set text
|
||||
let content = MastodonContent(content: status.content, emojis: status.emojiMeta)
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
cell.statusView.contentMetaText.configure(content: metaContent)
|
||||
} catch {
|
||||
cell.statusView.contentMetaText.textView.text = " "
|
||||
assertionFailure()
|
||||
}
|
||||
// set date
|
||||
cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow
|
||||
|
||||
cell.framePublisher
|
||||
.assign(to: \.value, on: self.repliedToCellFrame)
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .input(let replyToStatusObjectID, let attribute):
|
||||
let cell = self.composeStatusContentTableViewCell
|
||||
// configure header
|
||||
managedObjectContext.performAndWait {
|
||||
guard let replyToStatusObjectID = replyToStatusObjectID,
|
||||
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
return
|
||||
}
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback)
|
||||
}
|
||||
// configure author
|
||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
||||
// bind content warning
|
||||
attribute.isContentWarningComposing
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell, weak tableView] isContentWarningComposing in
|
||||
guard let cell = cell else { return }
|
||||
guard let tableView = tableView else { return }
|
||||
// self size input cell
|
||||
//tableView.
|
||||
cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
||||
cell.statusContentWarningEditorView.alpha = 0
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||
cell.statusContentWarningEditorView.alpha = 1
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.contentWarningContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView] text in
|
||||
guard let tableView = tableView else { return }
|
||||
// self size input cell
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
// bind input data
|
||||
attribute.contentWarningContent.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
// configure custom emoji picker
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
||||
// setup delegate
|
||||
cell.metaText.delegate = metaTextDelegate
|
||||
cell.metaText.textView.delegate = metaTextViewDelegate
|
||||
|
||||
return cell
|
||||
case .attachment(let attachmentService):
|
||||
return UITableViewCell()
|
||||
case .pollOption, .pollOptionAppendEntry, .pollExpiresOption:
|
||||
return UITableViewCell()
|
||||
}
|
||||
}
|
||||
self.dataSource = dataSource
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
||||
switch composeKind {
|
||||
case .reply(let statusObjectID):
|
||||
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
||||
snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo)
|
||||
case .hashtag, .mention, .post:
|
||||
snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status)
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func setupDiffableDataSource(
|
||||
for collectionView: UICollectionView,
|
||||
dependency: NeedsDependency,
|
||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||
metaTextDelegate: MetaTextDelegate,
|
||||
metaTextViewDelegate: UITextViewDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
) {
|
||||
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
|
||||
for: collectionView,
|
||||
dependency: dependency,
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
composeKind: composeKind,
|
||||
repliedToCellFrameSubscriber: repliedToCellFrame,
|
||||
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
||||
metaTextDelegate: metaTextDelegate,
|
||||
metaTextViewDelegate: metaTextViewDelegate,
|
||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
||||
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
)
|
||||
// content
|
||||
composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
|
||||
composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
|
||||
// attachment
|
||||
composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
|
||||
// poll
|
||||
composeStatusPollTableViewCell.delegate = self
|
||||
composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
|
||||
composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||
composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||
composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
|
||||
diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
||||
switch item {
|
||||
case .pollOption: return true
|
||||
default: return false
|
||||
// setup data source
|
||||
tableView.dataSource = self
|
||||
|
||||
attachmentServices
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
guard self.isViewAppeared else { return }
|
||||
|
||||
let cell = self.composeStatusAttachmentTableViewCell
|
||||
guard let dataSource = cell.dataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
|
||||
snapshot.appendSections([.main])
|
||||
let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) }
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update reordered data source
|
||||
diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isPollComposing,
|
||||
pollOptionAttributes
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isPollComposing, pollOptionAttributes 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)
|
||||
guard self.isViewAppeared else { return }
|
||||
|
||||
let cell = self.composeStatusPollTableViewCell
|
||||
guard let dataSource = cell.dataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
|
||||
snapshot.appendSections([.main])
|
||||
var items: [ComposeStatusPollItem] = []
|
||||
if isPollComposing {
|
||||
for attribute in pollOptionAttributes {
|
||||
items.append(.pollOption(attribute: attribute))
|
||||
}
|
||||
if pollOptionAttributes.count < 4 {
|
||||
items.append(.pollOptionAppendEntry)
|
||||
}
|
||||
items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
||||
}
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
self.pollOptionAttributes.value = pollOptionAttributes
|
||||
}
|
||||
|
||||
self.diffableDataSource = diffableDataSource
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
||||
switch composeKind {
|
||||
case .reply(let statusObjectID):
|
||||
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
||||
snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo)
|
||||
case .hashtag, .mention, .post:
|
||||
snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status)
|
||||
}
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
// some magic fix modal presentation animation issue
|
||||
collectionView.dataSource = diffableDataSource
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func setupCustomEmojiPickerDiffableDataSource(
|
||||
|
@ -246,3 +145,140 @@ extension ComposeViewModel {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension ComposeViewModel: UITableViewDataSource {
|
||||
|
||||
enum Section: CaseIterable {
|
||||
case repliedTo
|
||||
case status
|
||||
case attachment
|
||||
case poll
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return Section.allCases.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch Section.allCases[section] {
|
||||
case .repliedTo:
|
||||
switch composeKind {
|
||||
case .reply: return 1
|
||||
default: return 0
|
||||
}
|
||||
case .status: return 1
|
||||
case .attachment:
|
||||
return 1
|
||||
case .poll:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch Section.allCases[indexPath.section] {
|
||||
case .repliedTo:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
|
||||
guard case let .reply(statusObjectID) = composeKind else { return cell }
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
managedObjectContext.performAndWait {
|
||||
guard let replyTo = managedObjectContext.object(with: statusObjectID) as? Status else {
|
||||
return
|
||||
}
|
||||
let status = replyTo.reblog ?? replyTo
|
||||
|
||||
// set avatar
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||
// set name username
|
||||
cell.statusView.nameLabel.text = {
|
||||
let author = status.author
|
||||
return author.displayName.isEmpty ? author.username : author.displayName
|
||||
}()
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
// set text
|
||||
let content = MastodonContent(content: status.content, emojis: status.emojiMeta)
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
cell.statusView.contentMetaText.configure(content: metaContent)
|
||||
} catch {
|
||||
cell.statusView.contentMetaText.textView.text = " "
|
||||
assertionFailure()
|
||||
}
|
||||
// set date
|
||||
cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow
|
||||
|
||||
cell.framePublisher
|
||||
.assign(to: \.value, on: self.repliedToCellFrame)
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .status:
|
||||
let cell = self.composeStatusContentTableViewCell
|
||||
// configure header
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
managedObjectContext.performAndWait {
|
||||
guard case let .reply(replyToStatusObjectID) = self.composeKind,
|
||||
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
return
|
||||
}
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback)
|
||||
}
|
||||
// configure author
|
||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute)
|
||||
// bind content warning
|
||||
composeStatusAttribute.isContentWarningComposing
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell, weak tableView] isContentWarningComposing in
|
||||
guard let cell = cell else { return }
|
||||
guard let tableView = tableView else { return }
|
||||
// self size input cell
|
||||
//tableView.
|
||||
cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
||||
cell.statusContentWarningEditorView.alpha = 0
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||
cell.statusContentWarningEditorView.alpha = 1
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.contentWarningContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView, weak self] text in
|
||||
guard let tableView = tableView else { return }
|
||||
guard let self = self else { return }
|
||||
// self size input cell
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
// bind input data
|
||||
self.composeStatusAttribute.contentWarningContent.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
// configure custom emoji picker
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
||||
return cell
|
||||
case .attachment:
|
||||
let cell = self.composeStatusAttachmentTableViewCell
|
||||
return cell
|
||||
case .poll:
|
||||
let cell = self.composeStatusPollTableViewCell
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ComposeStatusPollTableViewCellDelegate
|
||||
extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
|
||||
func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
self.pollOptionAttributes.value = options
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import CoreDataStack
|
|||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
final class ComposeViewModel {
|
||||
final class ComposeViewModel: NSObject {
|
||||
|
||||
static let composeContentLimit: Int = 500
|
||||
|
||||
|
@ -33,11 +33,14 @@ final class ComposeViewModel {
|
|||
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
|
||||
let autoCompleteInfo = CurrentValueSubject<ComposeViewController.AutoCompleteInfo?, Never>(nil)
|
||||
var isViewAppeared = false
|
||||
|
||||
// output
|
||||
let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
|
||||
let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
|
||||
let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
|
||||
|
||||
var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>!
|
||||
private(set) lazy var publishStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
|
@ -77,8 +80,8 @@ final class ComposeViewModel {
|
|||
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||
|
||||
// polls
|
||||
let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([])
|
||||
let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute()
|
||||
let pollOptionAttributes = CurrentValueSubject<[ComposeStatusPollItem.PollOptionAttribute], Never>([])
|
||||
let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
|
@ -93,7 +96,9 @@ final class ComposeViewModel {
|
|||
self.selectedStatusVisibility = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value?.user.locked == true ? .private : .public)
|
||||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||
super.init()
|
||||
// end init
|
||||
|
||||
switch composeKind {
|
||||
case .reply(let repliedToStatusObjectID):
|
||||
context.managedObjectContext.performAndWait {
|
||||
|
@ -145,7 +150,7 @@ final class ComposeViewModel {
|
|||
case .post:
|
||||
self.preInsertedContent = nil
|
||||
}
|
||||
|
||||
|
||||
isCustomEmojiComposing
|
||||
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
||||
.store(in: &disposeBag)
|
||||
|
@ -284,45 +289,13 @@ final class ComposeViewModel {
|
|||
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind snapshot
|
||||
Publishers.CombineLatest3(
|
||||
attachmentServices.eraseToAnyPublisher(),
|
||||
isPollComposing.eraseToAnyPublisher(),
|
||||
pollOptionAttributes.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices, isPollComposing, pollAttributes in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: trigger attachments upload…", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
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(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)
|
||||
|
||||
// setup attribute updater
|
||||
Publishers.CombineLatest(
|
||||
attachmentServices,
|
||||
context.timestampUpdatePublisher
|
||||
)
|
||||
.sink { attachmentServices, _ in
|
||||
// drive service upload state
|
||||
// make image upload in the queue
|
||||
for attachmentService in attachmentServices {
|
||||
|
@ -395,7 +368,7 @@ extension ComposeViewModel {
|
|||
func createNewPollOptionIfPossible() {
|
||||
guard pollOptionAttributes.value.count < 4 else { return }
|
||||
|
||||
let attribute = ComposeStatusItem.ComposePollOptionAttribute()
|
||||
let attribute = ComposeStatusPollItem.PollOptionAttribute()
|
||||
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
||||
}
|
||||
|
||||
|
@ -467,7 +440,7 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
|||
|
||||
// MARK: - ComposePollAttributeDelegate
|
||||
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
||||
func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
|
||||
// trigger update
|
||||
pollOptionAttributes.value = pollOptionAttributes.value
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell {
|
|||
extension ComposeRepliedToStatusContentTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
statusView.actionToolbarContainer.isHidden = true
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
//
|
||||
// ComposeStatusAttachmentTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-29.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import AlamofireImage
|
||||
|
||||
final class ComposeStatusAttachmentTableViewCell: UITableViewCell {
|
||||
|
||||
private(set) var dataSource: UICollectionViewDiffableDataSource<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>!
|
||||
weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate?
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
private static func createLayout() -> UICollectionViewLayout {
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsetsReference = .readableContent
|
||||
return UICollectionViewCompositionalLayout(section: section)
|
||||
}
|
||||
|
||||
private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint!
|
||||
let collectionView: ComposeCollectionView = {
|
||||
let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout()
|
||||
let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
||||
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.isScrollEnabled = false
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusAttachmentTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(collectionView)
|
||||
collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh)
|
||||
NSLayoutConstraint.activate([
|
||||
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
collectionViewHeightLayoutConstraint,
|
||||
])
|
||||
|
||||
collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in
|
||||
guard let self = self else { return }
|
||||
self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||
weak self
|
||||
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
guard let self = self else { return UICollectionViewCell() }
|
||||
switch item {
|
||||
case .attachment(let attachmentService):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
||||
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
|
||||
cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate
|
||||
attachmentService.thumbnailImage
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] thumbnailImage in
|
||||
guard let cell = cell else { return }
|
||||
let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
|
||||
guard let image = thumbnailImage else {
|
||||
let placeholder = UIImage.placeholder(
|
||||
size: size,
|
||||
color: Asset.Colors.Background.systemGroupedBackground.color
|
||||
)
|
||||
.af.imageRounded(
|
||||
withCornerRadius: AttachmentContainerView.containerViewCornerRadius
|
||||
)
|
||||
cell.attachmentContainerView.previewImageView.image = placeholder
|
||||
return
|
||||
}
|
||||
cell.attachmentContainerView.previewImageView.image = image
|
||||
.af.imageAspectScaled(toFill: size)
|
||||
.af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
Publishers.CombineLatest(
|
||||
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
|
||||
attachmentService.error.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell, weak attachmentService] uploadState, error in
|
||||
guard let cell = cell else { return }
|
||||
guard let attachmentService = attachmentService else { return }
|
||||
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
|
||||
cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
|
||||
if let error = error {
|
||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||
cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
|
||||
} else {
|
||||
guard let uploadState = uploadState else { return }
|
||||
switch uploadState {
|
||||
case is MastodonAttachmentService.UploadState.Finish,
|
||||
is MastodonAttachmentService.UploadState.Fail:
|
||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||
cell.attachmentContainerView.emptyStateView.label.text = {
|
||||
if let file = attachmentService.file.value {
|
||||
switch file {
|
||||
case .jpeg, .png, .gif:
|
||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||
case .other:
|
||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
||||
}
|
||||
} else {
|
||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
NotificationCenter.default.publisher(
|
||||
for: UITextView.textDidChangeNotification,
|
||||
object: cell.attachmentContainerView.descriptionTextView
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { notification in
|
||||
guard let textField = notification.object as? UITextView else { return }
|
||||
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
attachmentService.description.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -24,10 +24,10 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
|
|||
static let metaTextViewTag: Int = 333
|
||||
let metaText: MetaText = {
|
||||
let metaText = MetaText()
|
||||
metaText.textView.tag = ComposeStatusContentCollectionViewCell.metaTextViewTag
|
||||
metaText.textView.backgroundColor = .clear
|
||||
metaText.textView.isScrollEnabled = false
|
||||
metaText.textView.keyboardType = .twitter
|
||||
metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset
|
||||
metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
metaText.textView.attributedPlaceholder = {
|
||||
var attributes = metaText.textAttributes
|
||||
|
@ -65,7 +65,7 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
|
|||
extension ComposeStatusContentTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
// selectionStyle = .none
|
||||
selectionStyle = .none
|
||||
layer.zPosition = 999
|
||||
preservesSuperviewLayoutMargins = true
|
||||
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
//
|
||||
// ComposeStatusPollTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-29.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol ComposeStatusPollTableViewCellDelegate: AnyObject {
|
||||
func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute])
|
||||
}
|
||||
|
||||
final class ComposeStatusPollTableViewCell: UITableViewCell {
|
||||
|
||||
private(set) var dataSource: UICollectionViewDiffableDataSource<ComposeStatusPollSection, ComposeStatusPollItem>!
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel?
|
||||
weak var delegate: ComposeStatusPollTableViewCellDelegate?
|
||||
weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate?
|
||||
weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate?
|
||||
weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
|
||||
|
||||
|
||||
private static func createLayout() -> UICollectionViewLayout {
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsetsReference = .readableContent
|
||||
return UICollectionViewCompositionalLayout(section: section)
|
||||
}
|
||||
|
||||
private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint!
|
||||
let collectionView: ComposeCollectionView = {
|
||||
let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout()
|
||||
let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||
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.Scene.Compose.background.color
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.isScrollEnabled = false
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(collectionView)
|
||||
collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh)
|
||||
NSLayoutConstraint.activate([
|
||||
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
collectionViewHeightLayoutConstraint,
|
||||
])
|
||||
|
||||
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeStatusPollTableViewCell.longPressReorderGestureHandler(_:)))
|
||||
collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||
|
||||
collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in
|
||||
guard let self = self else { return }
|
||||
print(collectionView.contentSize)
|
||||
self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||
weak self
|
||||
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
guard let self = self else { return UICollectionViewCell() }
|
||||
|
||||
switch item {
|
||||
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.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
|
||||
cell.pollOption
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: attribute.option)
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate
|
||||
if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel {
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
|
||||
}
|
||||
return cell
|
||||
case .pollOptionAppendEntry:
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
|
||||
cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||
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 { [weak cell] expiresOption in
|
||||
guard let cell = cell else { return }
|
||||
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.reorderingHandlers.canReorderItem = { item in
|
||||
switch item {
|
||||
case .pollOption: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
// update reordered data source
|
||||
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||
guard let self = self else { return }
|
||||
|
||||
let items = transaction.finalSnapshot.itemIdentifiers
|
||||
var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
|
||||
for item in items {
|
||||
guard case let .pollOption(attribute) = item else { continue }
|
||||
pollOptionAttributes.append(attribute)
|
||||
}
|
||||
self.delegate?.composeStatusPollTableViewCell(self, pollOptionAttributesDidReorder: pollOptionAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeStatusPollTableViewCell {
|
||||
|
||||
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
||||
switch(sender.state) {
|
||||
case .began:
|
||||
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)),
|
||||
let dataSource = self.dataSource else {
|
||||
break
|
||||
}
|
||||
guard let item = dataSource.itemIdentifier(for: selectedIndexPath),
|
||||
case .pollOption = item else {
|
||||
collectionView.cancelInteractiveMovement()
|
||||
return
|
||||
}
|
||||
|
||||
var position = sender.location(in: collectionView)
|
||||
position.x = collectionView.frame.width * 0.5
|
||||
collectionView.updateInteractiveMovementTargetPosition(position)
|
||||
case .ended:
|
||||
collectionView.endInteractiveMovement()
|
||||
collectionView.reloadData()
|
||||
default:
|
||||
collectionView.cancelInteractiveMovement()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -98,15 +98,15 @@ extension AudioContainerView {
|
|||
NSLayoutConstraint.activate([
|
||||
playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor),
|
||||
playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor),
|
||||
playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32),
|
||||
playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32),
|
||||
playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||
playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||
])
|
||||
|
||||
container.addArrangedSubview(slider)
|
||||
|
||||
container.addArrangedSubview(timeLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
timeLabel.widthAnchor.constraint(equalToConstant: 40),
|
||||
timeLabel.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue