Merge pull request #75 from tootsuite/feature/compose-misc
Implement general compose features
This commit is contained in:
commit
b9d9233e80
|
@ -216,6 +216,15 @@
|
|||
"one_day": "1 Day",
|
||||
"three_days": "3 Days",
|
||||
"seven_days": "7 Days"
|
||||
},
|
||||
"content_warning": {
|
||||
"placeholder": "Write an accurate warning here..."
|
||||
},
|
||||
"visibility": {
|
||||
"public": "Public",
|
||||
"unlisted": "Unlisted",
|
||||
"private": "Followers only",
|
||||
"direct": "Only people I mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */; };
|
||||
|
@ -241,10 +245,13 @@
|
|||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
|
||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
||||
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 +299,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 +423,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 +444,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>"; };
|
||||
|
@ -541,6 +554,7 @@
|
|||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
||||
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -555,7 +569,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 +578,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 +777,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||
DB49A61925FF327D00B98345 /* EmojiService */,
|
||||
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||
|
@ -770,7 +785,6 @@
|
|||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
||||
DB49A61925FF327D00B98345 /* EmojiService */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -830,6 +844,7 @@
|
|||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
@ -880,6 +895,7 @@
|
|||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||
);
|
||||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1106,6 +1122,9 @@
|
|||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
||||
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */,
|
||||
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */,
|
||||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1152,8 +1171,8 @@
|
|||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
|
||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
|
||||
);
|
||||
path = Compose;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1167,6 +1186,8 @@
|
|||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
|
||||
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */,
|
||||
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */,
|
||||
);
|
||||
path = CollectionViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1466,8 +1487,8 @@
|
|||
DB5086B725CC0D6400C2C187 /* Kingfisher */,
|
||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
|
||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||
DBE64A8A260C49D200E6359A /* TwitterTextEditor */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -1597,8 +1618,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 +1818,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 +1900,18 @@
|
|||
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 */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.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 +1945,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 +1963,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 +2585,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 +2593,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 +2643,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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -32,11 +32,16 @@ extension ComposeStatusItem {
|
|||
let username = CurrentValueSubject<String?, Never>(nil)
|
||||
let composeContent = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let contentWarningContent = CurrentValueSubject<String, Never>("")
|
||||
|
||||
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
||||
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
||||
lhs.displayName.value == rhs.displayName.value &&
|
||||
lhs.username.value == rhs.username.value &&
|
||||
lhs.composeContent.value == rhs.composeContent.value
|
||||
lhs.username.value == rhs.username.value &&
|
||||
lhs.composeContent.value == rhs.composeContent.value &&
|
||||
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
|
||||
lhs.contentWarningContent.value == rhs.contentWarningContent.value
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
|
|
@ -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,26 @@ 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,
|
||||
weak textEditorViewTextAttributesDelegate,
|
||||
weak composeStatusAttachmentTableViewCellDelegate,
|
||||
weak composeStatusPollOptionCollectionViewCellDelegate,
|
||||
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||
weak composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||
] 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 +75,33 @@ extension ComposeStatusSection {
|
|||
attribute.composeContent.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
attribute.isContentWarningComposing
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { isContentWarningComposing in
|
||||
// self size input cell
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
cell.statusContentWarningEditorView.containerView.isHidden = !isContentWarningComposing
|
||||
cell.statusContentWarningEditorView.alpha = 0
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||
cell.statusContentWarningEditorView.alpha = 1
|
||||
} completion: { _ in
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.contentWarningContent
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { text in
|
||||
// self size input cell
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
// bind input data
|
||||
attribute.contentWarningContent.value = text
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag)
|
||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
||||
|
||||
return cell
|
||||
case .attachment(let attachmentService):
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
||||
|
@ -136,6 +170,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 +193,7 @@ extension ComposeStatusSection {
|
|||
}
|
||||
|
||||
extension ComposeStatusSection {
|
||||
|
||||
static func configure(
|
||||
cell: ComposeStatusContentCollectionViewCell,
|
||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||
|
@ -177,7 +213,7 @@ extension ComposeStatusSection {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { displayName, username in
|
||||
cell.statusView.nameLabel.text = displayName
|
||||
cell.statusView.usernameLabel.text = username
|
||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
|
@ -187,4 +223,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
|
||||
}
|
||||
}
|
|
@ -75,10 +75,10 @@ internal enum Asset {
|
|||
internal static let buttonDefault = ColorAsset(name: "Colors/buttonDefault")
|
||||
internal static let buttonDisabled = ColorAsset(name: "Colors/buttonDisabled")
|
||||
internal static let buttonInactive = ColorAsset(name: "Colors/buttonInactive")
|
||||
internal static let danger = ColorAsset(name: "Colors/danger")
|
||||
internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow")
|
||||
internal static let lightBackground = ColorAsset(name: "Colors/lightBackground")
|
||||
internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue")
|
||||
internal static let lightDangerRed = ColorAsset(name: "Colors/lightDangerRed")
|
||||
internal static let lightDarkGray = ColorAsset(name: "Colors/lightDarkGray")
|
||||
internal static let lightDisabled = ColorAsset(name: "Colors/lightDisabled")
|
||||
internal static let lightInactive = ColorAsset(name: "Colors/lightInactive")
|
||||
|
|
|
@ -162,6 +162,10 @@ internal enum L10n {
|
|||
/// video
|
||||
internal static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video")
|
||||
}
|
||||
internal enum ContentWarning {
|
||||
/// Write an accurate warning here...
|
||||
internal static let placeholder = L10n.tr("Localizable", "Scene.Compose.ContentWarning.Placeholder")
|
||||
}
|
||||
internal enum MediaSelection {
|
||||
/// Browse
|
||||
internal static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse")
|
||||
|
@ -194,6 +198,16 @@ internal enum L10n {
|
|||
/// New Reply
|
||||
internal static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply")
|
||||
}
|
||||
internal enum Visibility {
|
||||
/// Only people I mention
|
||||
internal static let direct = L10n.tr("Localizable", "Scene.Compose.Visibility.Direct")
|
||||
/// Followers only
|
||||
internal static let `private` = L10n.tr("Localizable", "Scene.Compose.Visibility.Private")
|
||||
/// Public
|
||||
internal static let `public` = L10n.tr("Localizable", "Scene.Compose.Visibility.Public")
|
||||
/// Unlisted
|
||||
internal static let unlisted = L10n.tr("Localizable", "Scene.Compose.Visibility.Unlisted")
|
||||
}
|
||||
}
|
||||
internal enum ConfirmEmail {
|
||||
/// We just sent an email to %@,\ntap the link to confirm your account.
|
||||
|
|
|
@ -47,6 +47,7 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.Attachment.Video" = "video";
|
||||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
||||
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||
|
@ -59,6 +60,10 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.Poll.ThreeDays" = "3 Days";
|
||||
"Scene.Compose.Title.NewPost" = "New Post";
|
||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||
"Scene.Compose.Visibility.Direct" = "Only people I mention";
|
||||
"Scene.Compose.Visibility.Private" = "Followers only";
|
||||
"Scene.Compose.Visibility.Public" = "Public";
|
||||
"Scene.Compose.Visibility.Unlisted" = "Unlisted";
|
||||
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
||||
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
|
||||
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by MainasuK Cirno on 2021-3-11.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import TwitterTextEditor
|
||||
|
@ -15,6 +16,8 @@ final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
let statusView = StatusView()
|
||||
|
||||
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
||||
|
||||
let textEditorView: TextEditorView = {
|
||||
let textEditorView = TextEditorView()
|
||||
textEditorView.font = .preferredFont(forTextStyle: .body)
|
||||
|
@ -25,7 +28,9 @@ final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
|||
return textEditorView
|
||||
}()
|
||||
|
||||
// output
|
||||
let composeContent = PassthroughSubject<String, Never>()
|
||||
let contentWarningContent = PassthroughSubject<String, Never>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
@ -45,10 +50,20 @@ extension ComposeStatusContentCollectionViewCell {
|
|||
// selectionStyle = .none
|
||||
preservesSuperviewLayoutMargins = true
|
||||
|
||||
statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusContentWarningEditorView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusContentWarningEditorView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
statusContentWarningEditorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
statusContentWarningEditorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
])
|
||||
statusContentWarningEditorView.preservesSuperviewLayoutMargins = true
|
||||
statusContentWarningEditorView.containerBackgroundView.isHidden = false
|
||||
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
|
||||
statusView.topAnchor.constraint(equalTo: statusContentWarningEditorView.bottomAnchor, constant: 20),
|
||||
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
])
|
||||
|
@ -70,23 +85,39 @@ extension ComposeStatusContentCollectionViewCell {
|
|||
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
|
||||
// TODO:
|
||||
|
||||
|
||||
statusContentWarningEditorView.textView.delegate = self
|
||||
textEditorView.changeObserver = self
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
statusContentWarningEditorView.containerView.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITextViewDelegate
|
||||
// MARK: - TextEditorViewChangeObserver
|
||||
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
|
||||
guard changeResult.isTextChanged else { return }
|
||||
composeContent.send(textEditorView.text)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITextViewDelegate
|
||||
extension ComposeStatusContentCollectionViewCell: UITextViewDelegate {
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
// disable input line break
|
||||
guard text != "\n" else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textView.text)
|
||||
guard textView === statusContentWarningEditorView.textView else { return }
|
||||
// replace line break with space
|
||||
textView.text = textView.text.replacingOccurrences(of: "\n", with: " ")
|
||||
contentWarningContent.send(textView.text)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
|
||||
protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField)
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?)
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField)
|
||||
}
|
||||
|
@ -77,6 +78,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
|
||||
|
@ -131,6 +133,12 @@ extension ComposeStatusPollOptionCollectionViewCell: DeleteBackwardResponseTextF
|
|||
|
||||
// MARK: - UITextFieldDelegate
|
||||
extension ComposeStatusPollOptionCollectionViewCell: UITextFieldDelegate {
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.composeStatusPollOptionCollectionViewCell(self, textFieldDidBeginEditing: textField)
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
if textField === pollOptionView.optionTextField {
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import PhotosUI
|
||||
import Kingfisher
|
||||
import MastodonSDK
|
||||
import TwitterTextEditor
|
||||
|
||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
|
@ -52,9 +53,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!
|
||||
|
@ -86,14 +103,18 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
return documentPickerController
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
private static func createLayout() -> UICollectionViewLayout {
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100))
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.contentInsetsReference = .readableContent
|
||||
// section.interGroupSpacing = 10
|
||||
|
@ -153,6 +174,7 @@ extension ComposeViewController {
|
|||
viewModel.setupDiffableDataSource(
|
||||
for: collectionView,
|
||||
dependency: self,
|
||||
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
||||
textEditorViewTextAttributesDelegate: self,
|
||||
composeStatusAttachmentTableViewCellDelegate: self,
|
||||
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||
|
@ -162,15 +184,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 +212,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 {
|
||||
|
@ -206,22 +237,61 @@ extension ComposeViewController {
|
|||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind publish bar button state
|
||||
viewModel.isPublishBarButtonItemEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind media button toolbar state
|
||||
viewModel.isMediaToolbarButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind poll button toolbar state
|
||||
viewModel.isPollToolbarButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind custom emojis
|
||||
// bind image picker toolbar state
|
||||
viewModel.attachmentServices
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
self.composeToolbarView.mediaButton.isEnabled = attachmentServices.count < 4
|
||||
self.resetImagePicker()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind visibility toolbar UI
|
||||
viewModel.selectedStatusVisibility
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] type in
|
||||
guard let self = self else { return }
|
||||
self.composeToolbarView.visibilityButton.setImage(type.image, for: .normal)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.characterCount
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] characterCount in
|
||||
guard let self = self else { return }
|
||||
let count = ComposeViewModel.composeContentLimit - characterCount
|
||||
self.composeToolbarView.characterCountLabel.text = "\(count)"
|
||||
switch count {
|
||||
case _ where count < 0:
|
||||
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color
|
||||
default:
|
||||
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind text editor for custom emojis update event
|
||||
viewModel.customEmojiViewModel
|
||||
.compactMap { $0?.emojis }
|
||||
.switchToLatest()
|
||||
|
@ -235,14 +305,24 @@ extension ComposeViewController {
|
|||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind image picker toolbar state
|
||||
viewModel.attachmentServices
|
||||
// bind custom emoji picker UI
|
||||
viewModel.customEmojiViewModel
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] attachmentServices in
|
||||
guard let self = self else { return }
|
||||
self.composeToolbarView.mediaButton.isEnabled = attachmentServices.count < 4
|
||||
self.resetImagePicker()
|
||||
.map { viewModel -> AnyPublisher<[Mastodon.Entity.Emoji], Never> in
|
||||
guard let viewModel = viewModel else {
|
||||
return Just([]).eraseToAnyPublisher()
|
||||
}
|
||||
return viewModel.emojis.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.sink(receiveValue: { [weak self] emojis in
|
||||
guard let self = self else { return }
|
||||
if emojis.isEmpty {
|
||||
self.customEmojiPickerInputView.activityIndicatorView.startAnimating()
|
||||
} else {
|
||||
self.customEmojiPickerInputView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
|
@ -283,6 +363,25 @@ extension ComposeViewController {
|
|||
textEditorView()?.isEditing = true
|
||||
}
|
||||
|
||||
private func contentWarningEditorTextView() -> UITextView? {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
let items = diffableDataSource.snapshot().itemIdentifiers
|
||||
for item in items {
|
||||
switch item {
|
||||
case .input:
|
||||
guard let indexPath = diffableDataSource.indexPath(for: item),
|
||||
let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusContentCollectionViewCell else {
|
||||
continue
|
||||
}
|
||||
return cell.statusContentWarningEditorView.textView
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
||||
guard case .pollOption = item else { return nil }
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
|
@ -364,7 +463,7 @@ extension ComposeViewController {
|
|||
imagePicker.delegate = self
|
||||
return imagePicker
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewController {
|
||||
|
@ -553,6 +652,15 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
attributedString.addAttributes(attributes, range: match.range)
|
||||
}
|
||||
|
||||
if string.count > ComposeViewModel.composeContentLimit {
|
||||
var attributes = [NSAttributedString.Key: Any]()
|
||||
attributes[.foregroundColor] = Asset.Colors.danger.color
|
||||
let boundStart = string.index(string.startIndex, offsetBy: ComposeViewModel.composeContentLimit)
|
||||
let boundEnd = string.endIndex
|
||||
let range = boundStart..<boundEnd
|
||||
attributedString.addAttributes(attributes, range: NSRange(range, in: string))
|
||||
}
|
||||
|
||||
completion(attributedString)
|
||||
}
|
||||
}
|
||||
|
@ -563,8 +671,8 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
// MARK: - ComposeToolbarViewDelegate
|
||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
|
||||
switch mediaSelectionType {
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) {
|
||||
switch type {
|
||||
case .photoLibrary:
|
||||
present(imagePicker, animated: true, completion: nil)
|
||||
case .camera:
|
||||
|
@ -593,12 +701,28 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
|||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) {
|
||||
viewModel.isCustomEmojiComposing.value.toggle()
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
|
||||
// restore first responder for text editor when content warning dismiss
|
||||
if viewModel.isContentWarningComposing.value {
|
||||
if contentWarningEditorTextView()?.isFirstResponder == true {
|
||||
markTextEditorViewBecomeFirstResponser()
|
||||
}
|
||||
}
|
||||
|
||||
// toggle composing status
|
||||
viewModel.isContentWarningComposing.value.toggle()
|
||||
|
||||
// active content warning after toggled
|
||||
if viewModel.isContentWarningComposing.value {
|
||||
contentWarningEditorTextView()?.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton) {
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
|
||||
viewModel.selectedStatusVisibility.value = type
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -606,6 +730,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
|
||||
|
@ -707,6 +860,14 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega
|
|||
// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
|
||||
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
|
||||
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) {
|
||||
// FIXME: make poll section visible
|
||||
// DispatchQueue.main.async {
|
||||
// self.collectionView.scroll(to: .bottom, animated: true)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
// handle delete backward event for poll option input
|
||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
||||
guard (text ?? "").isEmpty else { return }
|
||||
|
|
|
@ -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,
|
||||
|
@ -51,7 +55,6 @@ extension ComposeViewModel {
|
|||
self.pollOptionAttributes.value = pollOptionAttributes
|
||||
}
|
||||
|
||||
|
||||
self.diffableDataSource = diffableDataSource
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
||||
|
@ -65,4 +68,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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,6 +61,15 @@ extension ComposeViewModel.PublishState {
|
|||
guard viewModel.isPollComposing.value else { return nil }
|
||||
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||
}()
|
||||
let sensitive: Bool = viewModel.isContentWarningComposing.value
|
||||
let spoilerText: String? = {
|
||||
let text = viewModel.composeStatusAttribute.contentWarningContent.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
}()
|
||||
let visibility = viewModel.selectedStatusVisibility.value.visibility
|
||||
|
||||
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
||||
|
@ -92,7 +101,10 @@ extension ComposeViewModel.PublishState {
|
|||
status: viewModel.composeStatusAttribute.composeContent.value,
|
||||
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||
pollOptions: pollOptions,
|
||||
pollExpiresIn: pollExpiresIn
|
||||
pollExpiresIn: pollExpiresIn,
|
||||
sensitive: sensitive,
|
||||
spoilerText: spoilerText,
|
||||
visibility: visibility
|
||||
)
|
||||
return viewModel.context.apiService.publishStatus(
|
||||
domain: domain,
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
// Created by MainasuK Cirno on 2021-3-11.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
final class ComposeViewModel {
|
||||
|
||||
static let composeContentLimit: Int = 500
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
|
@ -20,12 +24,15 @@ final class ComposeViewModel {
|
|||
let composeKind: ComposeStatusSection.ComposeKind
|
||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let selectedStatusVisibility = CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>(.public)
|
||||
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: [
|
||||
|
@ -44,9 +51,13 @@ final class ComposeViewModel {
|
|||
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let characterCount = CurrentValueSubject<Int, Never>(0)
|
||||
|
||||
// custom emojis
|
||||
var customEmojiViewModelSubscription: AnyCancellable?
|
||||
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
|
||||
let isLoadingCustomEmoji = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
// attachment
|
||||
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||
|
@ -69,6 +80,14 @@ final class ComposeViewModel {
|
|||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||
// end init
|
||||
|
||||
isCustomEmojiComposing
|
||||
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
isContentWarningComposing
|
||||
.assign(to: \.value, on: composeStatusAttribute.isContentWarningComposing)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind active authentication
|
||||
context.authenticationService.activeMastodonAuthentication
|
||||
.assign(to: \.value, on: activeAuthentication)
|
||||
|
@ -95,10 +114,30 @@ final class ComposeViewModel {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind character count
|
||||
Publishers.CombineLatest3(
|
||||
composeStatusAttribute.composeContent.eraseToAnyPublisher(),
|
||||
composeStatusAttribute.isContentWarningComposing.eraseToAnyPublisher(),
|
||||
composeStatusAttribute.contentWarningContent.eraseToAnyPublisher()
|
||||
)
|
||||
.map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
|
||||
let composeContent = composeContent ?? ""
|
||||
var count = composeContent.count
|
||||
if isContentWarningComposing {
|
||||
count += contentWarningContent.count
|
||||
}
|
||||
return count
|
||||
}
|
||||
.assign(to: \.value, on: characterCount)
|
||||
.store(in: &disposeBag)
|
||||
// bind compose bar button item UI state
|
||||
let isComposeContentEmpty = composeStatusAttribute.composeContent
|
||||
.map { ($0 ?? "").isEmpty }
|
||||
let isComposeContentValid = Just(true).eraseToAnyPublisher()
|
||||
let isComposeContentValid = composeStatusAttribute.composeContent
|
||||
.map { composeContent -> Bool in
|
||||
let composeContent = composeContent ?? ""
|
||||
return composeContent.count <= ComposeViewModel.composeContentLimit
|
||||
}
|
||||
let isMediaEmpty = attachmentServices
|
||||
.map { $0.isEmpty }
|
||||
let isMediaUploadAllSuccess = attachmentServices
|
||||
|
@ -264,6 +303,10 @@ final class ComposeViewModel {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeViewModel {
|
||||
|
@ -287,6 +330,6 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
|||
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
||||
// trigger update
|
||||
pollOptionAttributes.value = pollOptionAttributes.value
|
||||
// pollOptionAttributes.value = pollOptionAttributes.value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
protocol ComposeToolbarViewDelegate: class {
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton)
|
||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType)
|
||||
}
|
||||
|
||||
final class ComposeToolbarView: UIView {
|
||||
|
@ -58,6 +59,14 @@ final class ComposeToolbarView: UIView {
|
|||
return button
|
||||
}()
|
||||
|
||||
let characterCountLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
label.text = "500"
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -101,12 +110,23 @@ extension ComposeToolbarView {
|
|||
])
|
||||
}
|
||||
|
||||
characterCountLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(characterCountLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
characterCountLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||
characterCountLabel.leadingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: 8),
|
||||
characterCountLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||
characterCountLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
characterCountLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
|
||||
mediaButton.menu = createMediaContextMenu()
|
||||
mediaButton.showsMenuAsPrimaryAction = true
|
||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
|
||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
||||
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.visibilityButtonDidPressed(_:)), for: .touchUpInside)
|
||||
visibilityButton.menu = createVisibilityContextMenu()
|
||||
visibilityButton.showsMenuAsPrimaryAction = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,6 +136,40 @@ extension ComposeToolbarView {
|
|||
case photoLibrary
|
||||
case browse
|
||||
}
|
||||
|
||||
enum VisibilitySelectionType: String, CaseIterable {
|
||||
case `public`
|
||||
case unlisted
|
||||
case `private`
|
||||
case direct
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .public: return L10n.Scene.Compose.Visibility.public
|
||||
case .unlisted: return L10n.Scene.Compose.Visibility.unlisted
|
||||
case .private: return L10n.Scene.Compose.Visibility.private
|
||||
case .direct: return L10n.Scene.Compose.Visibility.direct
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .public: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
|
||||
case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
|
||||
case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))!
|
||||
}
|
||||
}
|
||||
|
||||
var visibility: Mastodon.Entity.Status.Visibility {
|
||||
switch self {
|
||||
case .public: return .public
|
||||
case .unlisted: return .unlisted
|
||||
case .private: return .private
|
||||
case .direct: return .direct
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeToolbarView {
|
||||
|
@ -154,9 +208,19 @@ extension ComposeToolbarView {
|
|||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
}
|
||||
|
||||
private func createVisibilityContextMenu() -> UIMenu {
|
||||
let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in
|
||||
UIAction(title: type.title, image: type.image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue)
|
||||
self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type)
|
||||
}
|
||||
}
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension ComposeToolbarView {
|
||||
|
||||
@objc private func pollButtonDidPressed(_ sender: UIButton) {
|
||||
|
@ -174,11 +238,6 @@ extension ComposeToolbarView {
|
|||
delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func visibilityButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.composeToolbarView(self, visibilityButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// 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
|
||||
}()
|
||||
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||
|
||||
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
|
||||
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(activityIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
|
||||
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),
|
||||
])
|
||||
|
||||
activityIndicatorView.hidesWhenStopped = true
|
||||
activityIndicatorView.startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// StatusContentWarningEditorView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class StatusContentWarningEditorView: UIView {
|
||||
|
||||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
return view
|
||||
}()
|
||||
|
||||
// due to section following readable inset. We overlap the bleeding to make backgorund fill
|
||||
// default hidden
|
||||
let containerBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
let iconImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = UIImage(systemName: "exclamationmark.shield")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 30, weight: .regular)).withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||
imageView.contentMode = .center
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let textView: UITextView = {
|
||||
let textView = UITextView()
|
||||
textView.font = .preferredFont(forTextStyle: .body)
|
||||
textView.isScrollEnabled = false
|
||||
textView.placeholder = L10n.Scene.Compose.ContentWarning.placeholder
|
||||
textView.backgroundColor = .clear
|
||||
return textView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusContentWarningEditorView {
|
||||
private func _init() {
|
||||
let contentWarningStackView = UIStackView()
|
||||
contentWarningStackView.axis = .horizontal
|
||||
contentWarningStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(contentWarningStackView)
|
||||
NSLayoutConstraint.activate([
|
||||
contentWarningStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
contentWarningStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
contentWarningStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
contentWarningStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
contentWarningStackView.addArrangedSubview(containerView)
|
||||
|
||||
containerBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(containerBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerBackgroundView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
containerBackgroundView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: -1024),
|
||||
containerBackgroundView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 1024),
|
||||
containerBackgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(iconImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
iconImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||
iconImageView.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.defaultHigh), // center alignment to avatar
|
||||
])
|
||||
iconImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(textView)
|
||||
NSLayoutConstraint.activate([
|
||||
textView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 6),
|
||||
textView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: StatusView.avatarToLabelSpacing - 4), // align to name label. minus magic 4pt to remove addtion inset
|
||||
textView.trailingAnchor.constraint(equalTo: containerView.readableContentGuide.trailingAnchor),
|
||||
containerView.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: 6),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct StatusContentWarningEditorView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
UIViewPreview(width: 375) {
|
||||
StatusContentWarningEditorView()
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 100))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -15,7 +15,7 @@ final class HomeTimelineNavigationBarView {
|
|||
}()
|
||||
|
||||
static let offlineView: UIView = {
|
||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightDangerRed.color)
|
||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.danger.color)
|
||||
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.offline)
|
||||
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
||||
return view
|
||||
|
|
|
@ -106,7 +106,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
|
||||
let usernameErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.lightDangerRed.color
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
return label
|
||||
}()
|
||||
|
@ -146,7 +146,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
|
||||
let emailErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.lightDangerRed.color
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
return label
|
||||
}()
|
||||
|
@ -177,7 +177,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
|
||||
let passwordErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.lightDangerRed.color
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
return label
|
||||
}()
|
||||
|
@ -201,7 +201,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
|
||||
let reasonErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.lightDangerRed.color
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
return label
|
||||
}()
|
||||
|
|
|
@ -198,10 +198,10 @@ extension MastodonRegisterViewModel {
|
|||
let attributeString = NSMutableAttributedString()
|
||||
|
||||
let image = MastodonRegisterViewModel.xmarkImage(font: font)
|
||||
attributeString.append(attributedStringImage(with: image, tintColor: Asset.Colors.lightDangerRed.color))
|
||||
attributeString.append(attributedStringImage(with: image, tintColor: Asset.Colors.danger.color))
|
||||
attributeString.append(NSAttributedString(string: " "))
|
||||
|
||||
let promptAttributedString = NSAttributedString(string: prompt, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.lightDangerRed.color])
|
||||
let promptAttributedString = NSAttributedString(string: prompt, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.danger.color])
|
||||
attributeString.append(promptAttributedString)
|
||||
|
||||
return attributeString
|
||||
|
|
|
@ -23,6 +23,7 @@ final class StatusView: UIView {
|
|||
|
||||
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
static let avatarToLabelSpacing: CGFloat = 5
|
||||
static let contentWarningBlurRadius: CGFloat = 12
|
||||
|
||||
static let boostIconImage: UIImage = {
|
||||
|
@ -249,7 +250,7 @@ extension StatusView {
|
|||
let authorContainerStackView = UIStackView()
|
||||
containerStackView.addArrangedSubview(authorContainerStackView)
|
||||
authorContainerStackView.axis = .horizontal
|
||||
authorContainerStackView.spacing = 5
|
||||
authorContainerStackView.spacing = StatusView.avatarToLabelSpacing
|
||||
|
||||
// avatar
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -98,12 +98,27 @@ extension Mastodon.API.Statuses {
|
|||
public let mediaIDs: [String]?
|
||||
public let pollOptions: [String]?
|
||||
public let pollExpiresIn: Int?
|
||||
public let sensitive: Bool?
|
||||
public let spoilerText: String?
|
||||
public let visibility: Mastodon.Entity.Status.Visibility?
|
||||
|
||||
public init(status: String?, mediaIDs: [String]?, pollOptions: [String]?, pollExpiresIn: Int?) {
|
||||
public init(
|
||||
status: String?,
|
||||
mediaIDs: [String]?,
|
||||
pollOptions: [String]?,
|
||||
pollExpiresIn: Int?,
|
||||
sensitive: Bool?,
|
||||
spoilerText: String?,
|
||||
visibility: Mastodon.Entity.Status.Visibility?
|
||||
) {
|
||||
self.status = status
|
||||
self.mediaIDs = mediaIDs
|
||||
self.pollOptions = pollOptions
|
||||
self.pollExpiresIn = pollExpiresIn
|
||||
self.sensitive = sensitive
|
||||
self.spoilerText = spoilerText
|
||||
self.visibility = visibility
|
||||
|
||||
}
|
||||
|
||||
var contentType: String? {
|
||||
|
@ -121,6 +136,9 @@ extension Mastodon.API.Statuses {
|
|||
data.append(Data.multipart(key: "poll[options][]", value: pollOption))
|
||||
}
|
||||
pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
|
||||
sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) }
|
||||
spoilerText.flatMap { data.append(Data.multipart(key: "spoiler_text", value: $0)) }
|
||||
visibility.flatMap { data.append(Data.multipart(key: "visibility", value: $0.rawValue)) }
|
||||
|
||||
data.append(Data.multipartEnd())
|
||||
return data
|
||||
|
|
Loading…
Reference in New Issue