diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 6b719bc71..d934ae236 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -120,6 +120,7 @@ DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; }; DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; }; DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; }; + DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; @@ -134,6 +135,11 @@ DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; }; DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; }; DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; }; + DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; }; + DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */; }; + DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */; }; + DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */; }; + DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */; }; DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481AC25EE155900BEFB67 /* Poll.swift */; }; DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B225EE16D000BEFB67 /* PollOption.swift */; }; DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; }; @@ -166,8 +172,6 @@ DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; }; DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; }; DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; }; - DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; }; - DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; @@ -245,6 +249,8 @@ DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; + DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; }; + DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -292,7 +298,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */, + DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -416,6 +422,7 @@ DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = ""; }; DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = ""; }; DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; + DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = ""; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; @@ -436,6 +443,11 @@ DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; }; DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = ""; }; + DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = ""; }; + DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = ""; }; + DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItem.swift; sourceTree = ""; }; + DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = ""; }; + DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = ""; }; DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = ""; }; DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = ""; }; DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; }; @@ -555,7 +567,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */, DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, @@ -565,6 +576,7 @@ DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, + DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */, 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -763,6 +775,7 @@ isa = PBXGroup; children = ( DB45FB0425CA87B4005A8AC7 /* APIService */, + DB49A61925FF327D00B98345 /* EmojiService */, DB9A489B26036E19008B817C /* MastodonAttachmentService */, DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */, DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */, @@ -770,7 +783,6 @@ 2DA6054625F716A2006356F9 /* PlaybackState.swift */, 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */, DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */, - DB49A61925FF327D00B98345 /* EmojiService */, ); path = Service; sourceTree = ""; @@ -830,6 +842,7 @@ DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, + DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, ); path = Section; sourceTree = ""; @@ -880,6 +893,7 @@ DB1E347725F519300079D7DF /* PickServerItem.swift */, DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, + DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, ); path = Item; sourceTree = ""; @@ -1106,6 +1120,8 @@ DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, + DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */, + DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */, ); path = View; sourceTree = ""; @@ -1167,6 +1183,8 @@ DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */, DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */, DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */, + DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */, + DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */, ); path = CollectionViewCell; sourceTree = ""; @@ -1466,8 +1484,8 @@ DB5086B725CC0D6400C2C187 /* Kingfisher */, 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, 2D939AC725EE14620076FA61 /* CropViewController */, - DB6672A225F9FDE500D60309 /* TwitterTextEditor */, DB9A487D2603456B008B817C /* UITextView+Placeholder */, + DBE64A8A260C49D200E6359A /* TwitterTextEditor */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -1597,8 +1615,8 @@ DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, - DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, + DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -1797,6 +1815,7 @@ 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, + DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, @@ -1878,14 +1897,17 @@ 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, + DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, + DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */, 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */, 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */, DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, + DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, @@ -1919,6 +1941,7 @@ 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, + DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, @@ -1936,6 +1959,7 @@ 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, 5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */, + DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */, DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */, @@ -2557,14 +2581,6 @@ minimumVersion = 6.1.0; }; }; - DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/twitter/TwitterTextEditor.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; @@ -2573,6 +2589,14 @@ minimumVersion = 1.4.1; }; }; + DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MainasuK/TwitterTextEditor"; + requirement = { + branch = "feature/input-view"; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2615,16 +2639,16 @@ package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - DB6672A225F9FDE500D60309 /* TwitterTextEditor */ = { - isa = XCSwiftPackageProductDependency; - package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; - productName = TwitterTextEditor; - }; DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { isa = XCSwiftPackageProductDependency; package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; + DBE64A8A260C49D200E6359A /* TwitterTextEditor */ = { + isa = XCSwiftPackageProductDependency; + package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; + productName = TwitterTextEditor; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 696ac7070..4f2051528 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -102,11 +102,11 @@ }, { "package": "TwitterTextEditor", - "repositoryURL": "https://github.com/twitter/TwitterTextEditor.git", + "repositoryURL": "https://github.com/MainasuK/TwitterTextEditor", "state": { - "branch": null, - "revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91", - "version": "1.0.0" + "branch": "feature/input-view", + "revision": "03e7b7497d424d96268f5bcca1f8e9955bb80fea", + "version": null } }, { diff --git a/Mastodon/Diffiable/Item/CustomEmojiPickerItem.swift b/Mastodon/Diffiable/Item/CustomEmojiPickerItem.swift new file mode 100644 index 000000000..52f522703 --- /dev/null +++ b/Mastodon/Diffiable/Item/CustomEmojiPickerItem.swift @@ -0,0 +1,36 @@ +// +// CustomEmojiPickerItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import Foundation +import MastodonSDK + +enum CustomEmojiPickerItem { + case emoji(attribute: CustomEmojiAttribute) +} + +extension CustomEmojiPickerItem: Equatable, Hashable { } + +extension CustomEmojiPickerItem { + final class CustomEmojiAttribute: Equatable, Hashable { + let id = UUID() + + let emoji: Mastodon.Entity.Emoji + + init(emoji: Mastodon.Entity.Emoji) { + self.emoji = emoji + } + + static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool { + return lhs.id == rhs.id && + lhs.emoji.shortcode == rhs.emoji.shortcode + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } +} diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 86bf9bd75..49f65a7c4 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -27,19 +27,19 @@ extension ComposeStatusSection { } extension ComposeStatusSection { - static func collectionViewDiffableDataSource( for collectionView: UICollectionView, dependency: NeedsDependency, managedObjectContext: NSManagedObjectContext, composeKind: ComposeKind, + customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate, composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in + UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak customEmojiPickerInputViewModel] collectionView, indexPath, item -> UICollectionViewCell? in switch item { case .replyTo(let repliedToStatusObjectID): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToTootContentCollectionViewCell @@ -68,6 +68,7 @@ extension ComposeStatusSection { attribute.composeContent.value = text } .store(in: &cell.disposeBag) + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag) return cell case .attachment(let attachmentService): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell @@ -136,6 +137,7 @@ extension ComposeStatusSection { .assign(to: \.value, on: attribute.option) .store(in: &cell.disposeBag) cell.delegate = composeStatusPollOptionCollectionViewCellDelegate + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) return cell case .pollOptionAppendEntry: let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell @@ -158,6 +160,7 @@ extension ComposeStatusSection { } extension ComposeStatusSection { + static func configure( cell: ComposeStatusContentCollectionViewCell, attribute: ComposeStatusItem.ComposeStatusAttribute @@ -187,4 +190,57 @@ extension ComposeStatusSection { .assign(to: \.value, on: attribute.composeContent) .store(in: &cell.disposeBag) } + +} + +protocol CustomEmojiReplacableTextInput: AnyObject { + var inputView: UIView? { get set } + func reloadInputViews() + + // UIKeyInput + func insertText(_ text: String) + // UIResponder + var isFirstResponder: Bool { get } +} + +class CustomEmojiReplacableTextInputReference { + weak var value: CustomEmojiReplacableTextInput? + + init(value: CustomEmojiReplacableTextInput? = nil) { + self.value = value + } +} + +extension TextEditorView: CustomEmojiReplacableTextInput { + func insertText(_ text: String) { + try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil) + } + + public override var isFirstResponder: Bool { + return isEditing + } + +} +extension UITextField: CustomEmojiReplacableTextInput { } +extension UITextView: CustomEmojiReplacableTextInput { } + +extension ComposeStatusSection { + + static func configureCustomEmojiPicker( + viewModel: CustomEmojiPickerInputViewModel?, + customEmojiReplacableTextInput: CustomEmojiReplacableTextInput, + disposeBag: inout Set + ) { + guard let viewModel = viewModel else { return } + viewModel.isCustomEmojiComposing + .receive(on: DispatchQueue.main) + .sink { [weak viewModel] isCustomEmojiComposing in + guard let viewModel = viewModel else { return } + customEmojiReplacableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil + customEmojiReplacableTextInput.reloadInputViews() + viewModel.append(customEmojiReplacableTextInput: customEmojiReplacableTextInput) + } + .store(in: &disposeBag) + } + } diff --git a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift new file mode 100644 index 000000000..2167d6f5c --- /dev/null +++ b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift @@ -0,0 +1,60 @@ +// +// CustomEmojiPickerSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import UIKit +import Kingfisher + +enum CustomEmojiPickerSection: Equatable, Hashable { + case emoji(name: String) +} + +extension CustomEmojiPickerSection { + static func collectionViewDiffableDataSource( + for collectionView: UICollectionView, + dependency: NeedsDependency + ) -> UICollectionViewDiffableDataSource { + let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in + switch item { + case .emoji(let attribute): + 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 + ) + return cell + } + } + + dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in + guard let dataSource = dataSource else { return nil } + let sections = dataSource.snapshot().sectionIdentifiers + guard indexPath.section < sections.count else { return nil } + let section = sections[indexPath.section] + + switch kind { + case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self): + 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 + } + return header + default: + assertionFailure() + return nil + } + } + + return dataSource + } +} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift index 8935d804e..712fea745 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift @@ -77,6 +77,7 @@ extension ComposeStatusPollOptionCollectionViewCell { reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) + reorderBarImageView.setContentCompressionResistancePriority(.defaultHigh + 10, for: .horizontal) pollOptionView.checkmarkImageView.isHidden = true pollOptionView.optionPercentageLabel.isHidden = true diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift new file mode 100644 index 000000000..61753a4c2 --- /dev/null +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift @@ -0,0 +1,42 @@ +// +// CustomEmojiPickerHeaderCollectionReusableView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import UIKit + +final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableView { + + 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 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension CustomEmojiPickerHeaderCollectionReusableView { + private func _init() { + 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), + ]) + } +} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift new file mode 100644 index 000000000..7acc49aeb --- /dev/null +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift @@ -0,0 +1,52 @@ +// +// CustomEmojiPickerItemCollectionViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import UIKit + +final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell { + + static let itemSize = CGSize(width: 44, height: 44) + + let emojiImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.layer.masksToBounds = true + return imageView + }() + + override var isHighlighted: Bool { + didSet { + emojiImageView.alpha = isHighlighted ? 0.5 : 1.0 + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension CustomEmojiPickerItemCollectionViewCell { + + private func _init() { + emojiImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(emojiImageView) + NSLayoutConstraint.activate([ + emojiImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + emojiImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + } + +} diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 19605ff38..abb486112 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -52,9 +52,25 @@ final class ComposeViewController: UIViewController, NeedsDependency { return collectionView }() + var systemKeyboardHeight: CGFloat = .zero { + didSet { + // note: some system AutoLayout warning here + customEmojiPickerInputView.frame.size.height = systemKeyboardHeight != .zero ? systemKeyboardHeight : 300 + } + } + + // CustomEmojiPickerView + let customEmojiPickerInputView: CustomEmojiPickerInputView = { + let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard) + return view + }() + let composeToolbarView: ComposeToolbarView = { let composeToolbarView = ComposeToolbarView() - composeToolbarView.backgroundColor = .secondarySystemBackground + let text = UITextView() + let inputView = UIInputView(frame: .init(x: 0, y: 0, width: 40, height: 40), inputViewStyle: .keyboard) + text.inputAccessoryView = inputView + composeToolbarView.backgroundColor = inputView.backgroundColor return composeToolbarView }() var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! @@ -153,6 +169,7 @@ extension ComposeViewController { viewModel.setupDiffableDataSource( for: collectionView, dependency: self, + customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, textEditorViewTextAttributesDelegate: self, composeStatusAttachmentTableViewCellDelegate: self, composeStatusPollOptionCollectionViewCellDelegate: self, @@ -162,15 +179,23 @@ extension ComposeViewController { let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:))) collectionView.addGestureRecognizer(longPressReorderGesture) + customEmojiPickerInputView.collectionView.delegate = self + viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView + viewModel.setupCustomEmojiPickerDiffableDataSource( + for: customEmojiPickerInputView.collectionView, + dependency: self + ) + // respond scrollView overlap change view.layoutIfNeeded() // update layout when keyboard show/dismiss - Publishers.CombineLatest3( + Publishers.CombineLatest4( KeyboardResponderService.shared.isShow.eraseToAnyPublisher(), KeyboardResponderService.shared.state.eraseToAnyPublisher(), - KeyboardResponderService.shared.endFrame.eraseToAnyPublisher() + KeyboardResponderService.shared.endFrame.eraseToAnyPublisher(), + viewModel.isCustomEmojiComposing.eraseToAnyPublisher() ) - .sink(receiveValue: { [weak self] isShow, state, endFrame in + .sink(receiveValue: { [weak self] isShow, state, endFrame, isCustomEmojiComposing in guard let self = self else { return } guard isShow, state == .dock else { @@ -182,8 +207,9 @@ extension ComposeViewController { } return } - // isShow AND dock state + self.systemKeyboardHeight = endFrame.height + let contentFrame = self.view.convert(self.collectionView.frame, to: nil) let padding = contentFrame.maxY - endFrame.minY guard padding > 0 else { @@ -593,6 +619,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate { } func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) { + viewModel.isCustomEmojiComposing.value.toggle() } func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) { @@ -606,6 +633,35 @@ extension ComposeViewController: ComposeToolbarViewDelegate { // MARK: - UITableViewDelegate extension ComposeViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) + + if collectionView === customEmojiPickerInputView.collectionView { + guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return } + let item = diffableDataSource.itemIdentifier(for: indexPath) + guard case let .emoji(attribute) = item else { return } + let emoji = attribute.emoji + let textEditorView = self.textEditorView() + + // retrive active text input and insert emoji + // the leading and trailing space is REQUIRED to fix `UITextStorage` layout issue + let reference = viewModel.customEmojiPickerInputViewModel.insertText(" :\(emoji.shortcode): ") + + // workaround: non-user interactive change do not trigger value update event + if reference?.value === textEditorView { + viewModel.composeStatusAttribute.composeContent.value = textEditorView?.text + // update text storage + textEditorView?.setNeedsUpdateTextAttributes() + // collection self-size + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.collectionView.collectionViewLayout.invalidateLayout() + } + } + } else { + // do nothing + } + } } // MARK: - UIAdaptivePresentationControllerDelegate diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift index 3f9f3d3fa..46bdbac1b 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -6,13 +6,16 @@ // import UIKit +import Combine import TwitterTextEditor +import MastodonSDK extension ComposeViewModel { func setupDiffableDataSource( for collectionView: UICollectionView, dependency: NeedsDependency, + customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate, composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, @@ -24,6 +27,7 @@ extension ComposeViewModel { dependency: dependency, managedObjectContext: context.managedObjectContext, composeKind: composeKind, + customEmojiPickerInputViewModel: customEmojiPickerInputViewModel, textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate, composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate, @@ -65,4 +69,49 @@ extension ComposeViewModel { diffableDataSource.apply(snapshot, animatingDifferences: false) } + func setupCustomEmojiPickerDiffableDataSource( + for collectionView: UICollectionView, + dependency: NeedsDependency + ) { + let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource( + for: collectionView, + dependency: dependency + ) + self.customEmojiPickerDiffableDataSource = diffableDataSource + + customEmojiViewModel + .sink { [weak self, weak diffableDataSource] customEmojiViewModel in + guard let self = self else { return } + guard let diffableDataSource = diffableDataSource else { return } + guard let customEmojiViewModel = customEmojiViewModel else { + self.customEmojiViewModelSubscription = nil + let snapshot = NSDiffableDataSourceSnapshot() + diffableDataSource.apply(snapshot) + return + } + + self.customEmojiViewModelSubscription = customEmojiViewModel.emojis + .receive(on: DispatchQueue.main) + .sink { [weak self, weak diffableDataSource] emojis in + guard let _ = self else { return } + guard let diffableDataSource = diffableDataSource else { return } + var snapshot = NSDiffableDataSourceSnapshot() + let customEmojiSection = CustomEmojiPickerSection.emoji(name: customEmojiViewModel.domain.uppercased()) + snapshot.appendSections([customEmojiSection]) + let items: [CustomEmojiPickerItem] = { + var items = [CustomEmojiPickerItem]() + for emoji in emojis where emoji.visibleInPicker { + let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji) + let item = CustomEmojiPickerItem.emoji(attribute: attribute) + items.append(item) + } + return items + }() + snapshot.appendItems(items, toSection: customEmojiSection) + diffableDataSource.apply(snapshot) + } + } + .store(in: &disposeBag) + } + } diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 80cd67695..095d37899 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -20,12 +20,13 @@ final class ComposeViewModel { let composeKind: ComposeStatusSection.ComposeKind let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute() let isPollComposing = CurrentValueSubject(false) + let isCustomEmojiComposing = CurrentValueSubject(false) let activeAuthentication: CurrentValueSubject let activeAuthenticationBox: CurrentValueSubject // output - //var diffableDataSource: UITableViewDiffableDataSource! var diffableDataSource: UICollectionViewDiffableDataSource! + var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource! private(set) lazy var publishStateMachine: GKStateMachine = { // exclude timeline middle fetcher state let stateMachine = GKStateMachine(states: [ @@ -46,7 +47,9 @@ final class ComposeViewModel { let isPollToolbarButtonEnabled = CurrentValueSubject(true) // custom emojis + var customEmojiViewModelSubscription: AnyCancellable? let customEmojiViewModel = CurrentValueSubject(nil) + let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() // attachment let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([]) @@ -69,6 +72,10 @@ final class ComposeViewModel { self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) // end init + isCustomEmojiComposing + .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing) + .store(in: &disposeBag) + // bind active authentication context.authenticationService.activeMastodonAuthentication .assign(to: \.value, on: activeAuthentication) diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift new file mode 100644 index 000000000..87b1ee481 --- /dev/null +++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift @@ -0,0 +1,74 @@ +// +// CustomEmojiPickerInputView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import UIKit + +final class CustomEmojiPickerInputView: UIInputView { + + private(set) lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()) + collectionView.register(CustomEmojiPickerItemCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self)) + collectionView.register(CustomEmojiPickerHeaderCollectionReusableView.self, forSupplementaryViewOfKind: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self)) + collectionView.backgroundColor = .clear + return collectionView + }() + + override init(frame: CGRect, inputViewStyle: UIInputView.Style) { + super.init(frame: frame, inputViewStyle: inputViewStyle) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension CustomEmojiPickerInputView { + private func _init() { + allowsSelfSizing = true + + collectionView.translatesAutoresizingMaskIntoConstraints = false + addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: topAnchor), + collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } +} + +extension CustomEmojiPickerInputView { + func createLayout() -> UICollectionViewLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(CustomEmojiPickerItemCollectionViewCell.itemSize.width), + heightDimension: .fractionalHeight(1.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(4), top: .flexible(4), trailing: .flexible(0), bottom: .flexible(0)) + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .absolute(CustomEmojiPickerItemCollectionViewCell.itemSize.height)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.interGroupSpacing = 5 + section.contentInsetsReference = .readableContent + section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0) + + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(44)), + elementKind: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), + alignment: .top) + // sectionHeader.pinToVisibleBounds = true + sectionHeader.zIndex = 2 + section.boundarySupplementaryItems = [sectionHeader] + + let layout = UICollectionViewCompositionalLayout(section: section) + return layout + } +} diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift new file mode 100644 index 000000000..02a45d922 --- /dev/null +++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift @@ -0,0 +1,58 @@ +// +// CustomEmojiPickerInputViewModel.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-25. +// + +import UIKit +import Combine + +final class CustomEmojiPickerInputViewModel { + + var disposeBag = Set() + + private var customEmojiReplacableTextInputReferences: [CustomEmojiReplacableTextInputReference] = [] + + // input + weak var customEmojiPickerInputView: CustomEmojiPickerInputView? + + // output + let isCustomEmojiComposing = CurrentValueSubject(false) + +} + +extension CustomEmojiPickerInputViewModel { + + private func removeEmptyReferences() { + customEmojiReplacableTextInputReferences.removeAll(where: { element in + element.value == nil + }) + } + + func append(customEmojiReplacableTextInput textInput: CustomEmojiReplacableTextInput) { + removeEmptyReferences() + + let isContains = customEmojiReplacableTextInputReferences.contains(where: { element in + element.value === textInput + }) + guard !isContains else { + return + } + customEmojiReplacableTextInputReferences.append(CustomEmojiReplacableTextInputReference(value: textInput)) + } + + func insertText(_ text: String) -> CustomEmojiReplacableTextInputReference? { + removeEmptyReferences() + + for reference in customEmojiReplacableTextInputReferences { + guard reference.value?.isFirstResponder == true else { continue } + reference.value?.insertText(text) + return reference + } + + return nil + } + +} +