forked from zelo72/mastodon-ios
feat: implement emoji picker
This commit is contained in:
parent
91cd7322e7
commit
df66cc6b4a
|
@ -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 = "<group>"; };
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
|
||||
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
|
||||
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -436,6 +443,11 @@
|
|||
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
|
||||
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = "<group>"; };
|
||||
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = "<group>"; };
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = "<group>"; };
|
||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItem.swift; sourceTree = "<group>"; };
|
||||
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
|
||||
DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
|
||||
DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = "<group>"; };
|
||||
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -830,6 +842,7 @@
|
|||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
@ -880,6 +893,7 @@
|
|||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||
);
|
||||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1106,6 +1120,8 @@
|
|||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
||||
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */,
|
||||
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1167,6 +1183,8 @@
|
|||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
|
||||
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */,
|
||||
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */,
|
||||
);
|
||||
path = CollectionViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 */
|
||||
|
|
|
@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ComposeStatusSection, ComposeStatusItem> {
|
||||
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<AnyCancellable>
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<CustomEmojiPickerSection, CustomEmojiPickerItem> {
|
||||
let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<CustomEmojiPickerSection, CustomEmojiPickerItem>()
|
||||
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<CustomEmojiPickerSection, CustomEmojiPickerItem>()
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,12 +20,13 @@ final class ComposeViewModel {
|
|||
let composeKind: ComposeStatusSection.ComposeKind
|
||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||
|
||||
// output
|
||||
//var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>!
|
||||
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<Bool, Never>(true)
|
||||
|
||||
// custom emojis
|
||||
var customEmojiViewModelSubscription: AnyCancellable?
|
||||
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
private var customEmojiReplacableTextInputReferences: [CustomEmojiReplacableTextInputReference] = []
|
||||
|
||||
// input
|
||||
weak var customEmojiPickerInputView: CustomEmojiPickerInputView?
|
||||
|
||||
// output
|
||||
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(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
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue