Merge pull request #186 from tootsuite/feature/meta-textview
Migrate compose scene from collection view to table view
This commit is contained in:
commit
d92c1b2ca4
|
@ -188,7 +188,11 @@
|
||||||
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; };
|
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; };
|
||||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
|
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
|
||||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
||||||
DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */; };
|
DB03F7EB268976B5007B274C /* MastodonMeta in Frameworks */ = {isa = PBXBuildFile; productRef = DB03F7EA268976B5007B274C /* MastodonMeta */; };
|
||||||
|
DB03F7ED268976B5007B274C /* MetaTextView in Frameworks */ = {isa = PBXBuildFile; productRef = DB03F7EC268976B5007B274C /* MetaTextView */; };
|
||||||
|
DB03F7F026899097007B274C /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7EF26899097007B274C /* ComposeStatusContentTableViewCell.swift */; };
|
||||||
|
DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; };
|
||||||
|
DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; };
|
||||||
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
|
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
|
||||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||||
|
@ -221,6 +225,13 @@
|
||||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
||||||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
||||||
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
||||||
|
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; };
|
||||||
|
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; };
|
||||||
|
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; };
|
||||||
|
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */; };
|
||||||
|
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */; };
|
||||||
|
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; };
|
||||||
|
DB3667CA268B14A80027D07F /* ReplicaStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667C9268B14A80027D07F /* ReplicaStatusView.swift */; };
|
||||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||||
|
@ -339,8 +350,6 @@
|
||||||
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; };
|
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; };
|
||||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
||||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */; };
|
|
||||||
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; };
|
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; };
|
||||||
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
|
||||||
|
@ -799,7 +808,9 @@
|
||||||
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = "<group>"; };
|
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = "<group>"; };
|
||||||
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCollectionView.swift; sourceTree = "<group>"; };
|
DB03F7EF26899097007B274C /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = "<group>"; };
|
||||||
DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
||||||
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -834,6 +845,13 @@
|
||||||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRelationshipActionButton.swift; sourceTree = "<group>"; };
|
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRelationshipActionButton.swift; sourceTree = "<group>"; };
|
||||||
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = "<group>"; };
|
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = "<group>"; };
|
||||||
DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
|
DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
|
||||||
|
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = "<group>"; };
|
||||||
|
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = "<group>"; };
|
||||||
|
DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = "<group>"; };
|
||||||
|
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = "<group>"; };
|
||||||
|
DB3667C9268B14A80027D07F /* ReplicaStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplicaStatusView.swift; sourceTree = "<group>"; };
|
||||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -949,8 +967,6 @@
|
||||||
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = "<group>"; };
|
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = "<group>"; };
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = "<group>"; };
|
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1120,6 +1136,7 @@
|
||||||
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */,
|
DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */,
|
||||||
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
||||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||||
|
DB03F7ED268976B5007B274C /* MetaTextView in Frameworks */,
|
||||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
||||||
|
@ -1138,6 +1155,7 @@
|
||||||
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
|
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
|
||||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */,
|
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */,
|
||||||
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */,
|
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */,
|
||||||
|
DB03F7EB268976B5007B274C /* MastodonMeta in Frameworks */,
|
||||||
DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */,
|
DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1519,6 +1537,8 @@
|
||||||
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
|
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
|
||||||
|
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
|
||||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||||
|
@ -1583,6 +1603,8 @@
|
||||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||||
|
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */,
|
||||||
|
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */,
|
||||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||||
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
||||||
|
@ -1732,6 +1754,17 @@
|
||||||
path = Status;
|
path = Status;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB03F7F1268990A2007B274C /* TableViewCell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */,
|
||||||
|
DB03F7EF26899097007B274C /* ComposeStatusContentTableViewCell.swift */,
|
||||||
|
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||||
|
DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = TableViewCell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB084B5125CBC56300F898ED /* CoreDataStack */ = {
|
DB084B5125CBC56300F898ED /* CoreDataStack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1940,7 +1973,8 @@
|
||||||
DB55D32225FB4D320002F825 /* View */ = {
|
DB55D32225FB4D320002F825 /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */,
|
DB3667C9268B14A80027D07F /* ReplicaStatusView.swift */,
|
||||||
|
DB03F7F42689B782007B274C /* ComposeTableView.swift */,
|
||||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
||||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
||||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
||||||
|
@ -2082,6 +2116,7 @@
|
||||||
DB6F5E36264E78EA009108F4 /* AutoComplete */,
|
DB6F5E36264E78EA009108F4 /* AutoComplete */,
|
||||||
DB55D32225FB4D320002F825 /* View */,
|
DB55D32225FB4D320002F825 /* View */,
|
||||||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
||||||
|
DB03F7F1268990A2007B274C /* TableViewCell */,
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
||||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
||||||
|
@ -2093,8 +2128,6 @@
|
||||||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
|
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
|
||||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */,
|
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */,
|
||||||
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||||
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||||
|
@ -2653,6 +2686,8 @@
|
||||||
DBAC64A0267E6D02007FE9FD /* Fuzi */,
|
DBAC64A0267E6D02007FE9FD /* Fuzi */,
|
||||||
DBF7A0FB26830C33004176A2 /* FPSIndicator */,
|
DBF7A0FB26830C33004176A2 /* FPSIndicator */,
|
||||||
DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */,
|
DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */,
|
||||||
|
DB03F7EA268976B5007B274C /* MastodonMeta */,
|
||||||
|
DB03F7EC268976B5007B274C /* MetaTextView */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -2846,6 +2881,7 @@
|
||||||
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
|
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
|
||||||
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
|
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
|
||||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
|
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
|
||||||
|
DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -3193,6 +3229,7 @@
|
||||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||||
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
|
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
|
||||||
|
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */,
|
||||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||||
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
|
||||||
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */,
|
||||||
|
@ -3216,8 +3253,10 @@
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */,
|
DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */,
|
||||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
|
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
|
||||||
|
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */,
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||||
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
|
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
|
||||||
|
DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */,
|
||||||
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
|
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
|
||||||
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
|
@ -3304,17 +3343,18 @@
|
||||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||||
|
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */,
|
||||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||||
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */,
|
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */,
|
||||||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */,
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */,
|
||||||
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
||||||
|
DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||||
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
||||||
|
@ -3325,6 +3365,7 @@
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
|
DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */,
|
||||||
2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */,
|
2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */,
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
|
@ -3400,6 +3441,7 @@
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||||
|
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||||
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
|
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||||
|
@ -3440,8 +3482,8 @@
|
||||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||||
DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */,
|
|
||||||
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
||||||
|
DB3667CA268B14A80027D07F /* ReplicaStatusView.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
|
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */,
|
||||||
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */,
|
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */,
|
||||||
|
@ -3468,9 +3510,11 @@
|
||||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
||||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||||
|
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */,
|
||||||
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */,
|
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */,
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */,
|
DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */,
|
||||||
|
DB03F7F026899097007B274C /* ComposeStatusContentTableViewCell.swift in Sources */,
|
||||||
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
||||||
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
||||||
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
|
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
|
||||||
|
@ -3489,7 +3533,6 @@
|
||||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
|
DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
|
||||||
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
|
||||||
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
||||||
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
||||||
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */,
|
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */,
|
||||||
|
@ -4730,6 +4773,14 @@
|
||||||
minimumVersion = 0.1.1;
|
minimumVersion = 0.1.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/TwidereProject/MetaTextView.git";
|
||||||
|
requirement = {
|
||||||
|
kind = exactVersion;
|
||||||
|
version = 1.2.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
|
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git";
|
repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git";
|
||||||
|
@ -4863,6 +4914,16 @@
|
||||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||||
productName = CommonOSLog;
|
productName = CommonOSLog;
|
||||||
};
|
};
|
||||||
|
DB03F7EA268976B5007B274C /* MastodonMeta */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
|
||||||
|
productName = MastodonMeta;
|
||||||
|
};
|
||||||
|
DB03F7EC268976B5007B274C /* MetaTextView */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
|
||||||
|
productName = MetaTextView;
|
||||||
|
};
|
||||||
DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */ = {
|
DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */;
|
package = DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>20</integer>
|
<integer>23</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
@ -109,6 +109,15 @@
|
||||||
"version": "6.2.1"
|
"version": "6.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "MetaTextView",
|
||||||
|
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "2660fc30ef6ed8de347ddca499341a965d1fda56",
|
||||||
|
"version": "1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "Nuke",
|
"package": "Nuke",
|
||||||
"repositoryURL": "https://github.com/kean/Nuke.git",
|
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusAttachmentItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ComposeStatusAttachmentItem {
|
||||||
|
case attachment(attachmentService: MastodonAttachmentService)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusAttachmentItem: Hashable { }
|
|
@ -13,14 +13,10 @@ import CoreData
|
||||||
enum ComposeStatusItem {
|
enum ComposeStatusItem {
|
||||||
case replyTo(statusObjectID: NSManagedObjectID)
|
case replyTo(statusObjectID: NSManagedObjectID)
|
||||||
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
||||||
case attachment(attachmentService: MastodonAttachmentService)
|
case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute)
|
||||||
case pollOption(attribute: ComposePollOptionAttribute)
|
case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute)
|
||||||
case pollOptionAppendEntry
|
|
||||||
case pollExpiresOption(attribute: ComposePollExpiresOptionAttribute)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusItem: Equatable { }
|
|
||||||
|
|
||||||
extension ComposeStatusItem: Hashable { }
|
extension ComposeStatusItem: Hashable { }
|
||||||
|
|
||||||
extension ComposeStatusItem {
|
extension ComposeStatusItem {
|
||||||
|
@ -50,88 +46,22 @@ extension ComposeStatusItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol ComposePollAttributeDelegate: AnyObject {
|
|
||||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?)
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ComposeStatusItem {
|
extension ComposeStatusItem {
|
||||||
final class ComposePollOptionAttribute: Equatable, Hashable {
|
final class ComposeStatusAttachmentAttribute: Hashable {
|
||||||
private let id = UUID()
|
private let id = UUID()
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
weak var delegate: ComposePollAttributeDelegate?
|
|
||||||
|
|
||||||
let option = CurrentValueSubject<String, Never>("")
|
var attachmentServices: [MastodonAttachmentService]
|
||||||
|
|
||||||
init() {
|
init(attachmentServices: [MastodonAttachmentService]) {
|
||||||
option
|
self.attachmentServices = attachmentServices
|
||||||
.sink { [weak self] option in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.delegate?.composePollAttribute(self, pollOptionDidChange: option)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
static func == (lhs: ComposeStatusAttachmentAttribute, rhs: ComposeStatusAttachmentAttribute) -> Bool {
|
||||||
disposeBag.removeAll()
|
return lhs.attachmentServices == rhs.attachmentServices
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: ComposePollOptionAttribute, rhs: ComposePollOptionAttribute) -> Bool {
|
|
||||||
return lhs.id == rhs.id &&
|
|
||||||
lhs.option.value == rhs.option.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusItem {
|
|
||||||
final class ComposePollExpiresOptionAttribute: Equatable, Hashable {
|
|
||||||
private let id = UUID()
|
|
||||||
|
|
||||||
let expiresOption = CurrentValueSubject<ExpiresOption, Never>(.thirtyMinutes)
|
|
||||||
|
|
||||||
|
|
||||||
static func == (lhs: ComposePollExpiresOptionAttribute, rhs: ComposePollExpiresOptionAttribute) -> Bool {
|
|
||||||
return lhs.id == rhs.id &&
|
|
||||||
lhs.expiresOption.value == rhs.expiresOption.value
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ExpiresOption: Equatable, Hashable, CaseIterable {
|
|
||||||
case thirtyMinutes
|
|
||||||
case oneHour
|
|
||||||
case sixHours
|
|
||||||
case oneDay
|
|
||||||
case threeDays
|
|
||||||
case sevenDays
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes
|
|
||||||
case .oneHour: return L10n.Scene.Compose.Poll.oneHour
|
|
||||||
case .sixHours: return L10n.Scene.Compose.Poll.sixHours
|
|
||||||
case .oneDay: return L10n.Scene.Compose.Poll.oneDay
|
|
||||||
case .threeDays: return L10n.Scene.Compose.Poll.threeDays
|
|
||||||
case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var seconds: Int {
|
|
||||||
switch self {
|
|
||||||
case .thirtyMinutes: return 60 * 30
|
|
||||||
case .oneHour: return 60 * 60 * 1
|
|
||||||
case .sixHours: return 60 * 60 * 6
|
|
||||||
case .oneDay: return 60 * 60 * 24
|
|
||||||
case .threeDays: return 60 * 60 * 24 * 3
|
|
||||||
case .sevenDays: return 60 * 60 * 24 * 7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusPollItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
enum ComposeStatusPollItem {
|
||||||
|
case pollOption(attribute: PollOptionAttribute)
|
||||||
|
case pollOptionAppendEntry
|
||||||
|
case pollExpiresOption(attribute: PollExpiresOptionAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollItem: Hashable { }
|
||||||
|
|
||||||
|
extension ComposeStatusPollItem {
|
||||||
|
|
||||||
|
final class PollOptionAttribute: Equatable, Hashable {
|
||||||
|
private let id = UUID()
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
weak var delegate: ComposePollAttributeDelegate?
|
||||||
|
|
||||||
|
let option = CurrentValueSubject<String, Never>("")
|
||||||
|
|
||||||
|
init() {
|
||||||
|
option
|
||||||
|
.sink { [weak self] option in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.composePollAttribute(self, pollOptionDidChange: option)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: PollOptionAttribute, rhs: PollOptionAttribute) -> Bool {
|
||||||
|
return lhs.id == rhs.id &&
|
||||||
|
lhs.option.value == rhs.option.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol ComposePollAttributeDelegate: AnyObject {
|
||||||
|
func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollItem {
|
||||||
|
final class PollExpiresOptionAttribute: Equatable, Hashable {
|
||||||
|
private let id = UUID()
|
||||||
|
|
||||||
|
let expiresOption = CurrentValueSubject<ExpiresOption, Never>(.thirtyMinutes)
|
||||||
|
|
||||||
|
|
||||||
|
static func == (lhs: PollExpiresOptionAttribute, rhs: PollExpiresOptionAttribute) -> Bool {
|
||||||
|
return lhs.id == rhs.id &&
|
||||||
|
lhs.expiresOption.value == rhs.expiresOption.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExpiresOption: Equatable, Hashable, CaseIterable {
|
||||||
|
case thirtyMinutes
|
||||||
|
case oneHour
|
||||||
|
case sixHours
|
||||||
|
case oneDay
|
||||||
|
case threeDays
|
||||||
|
case sevenDays
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes
|
||||||
|
case .oneHour: return L10n.Scene.Compose.Poll.oneHour
|
||||||
|
case .sixHours: return L10n.Scene.Compose.Poll.sixHours
|
||||||
|
case .oneDay: return L10n.Scene.Compose.Poll.oneDay
|
||||||
|
case .threeDays: return L10n.Scene.Compose.Poll.threeDays
|
||||||
|
case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds: Int {
|
||||||
|
switch self {
|
||||||
|
case .thirtyMinutes: return 60 * 30
|
||||||
|
case .oneHour: return 60 * 60 * 1
|
||||||
|
case .sixHours: return 60 * 60 * 6
|
||||||
|
case .oneDay: return 60 * 60 * 24
|
||||||
|
case .threeDays: return 60 * 60 * 24 * 3
|
||||||
|
case .sevenDays: return 60 * 60 * 24 * 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusAttachmentSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ComposeStatusAttachmentSection: Hashable {
|
||||||
|
case main
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusPollSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ComposeStatusPollSection: Hashable {
|
||||||
|
case main
|
||||||
|
}
|
|
@ -9,7 +9,8 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import TwitterTextEditor
|
import MetaTextView
|
||||||
|
import MastodonMeta
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
|
|
||||||
enum ComposeStatusSection: Equatable, Hashable {
|
enum ComposeStatusSection: Equatable, Hashable {
|
||||||
|
@ -29,232 +30,9 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
static func collectionViewDiffableDataSource(
|
|
||||||
for collectionView: UICollectionView,
|
|
||||||
dependency: NeedsDependency,
|
|
||||||
managedObjectContext: NSManagedObjectContext,
|
|
||||||
composeKind: ComposeKind,
|
|
||||||
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
|
||||||
textEditorViewChangeObserver: TextEditorViewChangeObserver,
|
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
|
||||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
||||||
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
|
||||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
|
||||||
weak customEmojiPickerInputViewModel,
|
|
||||||
weak textEditorViewTextAttributesDelegate,
|
|
||||||
weak textEditorViewChangeObserver,
|
|
||||||
weak composeStatusAttachmentTableViewCellDelegate,
|
|
||||||
weak composeStatusPollOptionCollectionViewCellDelegate,
|
|
||||||
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
|
||||||
weak composeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
||||||
] collectionView, indexPath, item -> UICollectionViewCell? in
|
|
||||||
switch item {
|
|
||||||
case .replyTo(let replyToStatusObjectID):
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
|
|
||||||
// set empty text before retrieve real data to fix pseudo-text display issue
|
|
||||||
cell.statusView.nameLabel.text = " "
|
|
||||||
cell.statusView.usernameLabel.text = " "
|
|
||||||
managedObjectContext.performAndWait {
|
|
||||||
guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let status = replyTo.reblog ?? replyTo
|
|
||||||
|
|
||||||
// set avatar
|
|
||||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
|
||||||
// set name username
|
|
||||||
cell.statusView.nameLabel.text = {
|
|
||||||
let author = status.author
|
|
||||||
return author.displayName.isEmpty ? author.username : author.displayName
|
|
||||||
}()
|
|
||||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
|
||||||
// set text
|
|
||||||
//status.emoji
|
|
||||||
cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:])
|
|
||||||
// set date
|
|
||||||
cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow
|
|
||||||
|
|
||||||
cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag)
|
|
||||||
}
|
|
||||||
return cell
|
|
||||||
case .input(let replyToStatusObjectID, let attribute):
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
|
||||||
cell.statusContentWarningEditorView.textView.text = attribute.contentWarningContent.value
|
|
||||||
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
|
||||||
managedObjectContext.performAndWait {
|
|
||||||
guard let replyToStatusObjectID = replyToStatusObjectID,
|
|
||||||
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
|
||||||
cell.statusView.headerContainerView.isHidden = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cell.statusView.headerContainerView.isHidden = false
|
|
||||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
|
||||||
cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback)
|
|
||||||
}
|
|
||||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
|
||||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
|
||||||
cell.textEditorViewChangeObserver = textEditorViewChangeObserver // relay
|
|
||||||
cell.composeContent
|
|
||||||
.removeDuplicates()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak collectionView] text in
|
|
||||||
guard let collectionView = collectionView else { return }
|
|
||||||
// self size input cell
|
|
||||||
// needs restore content offset to resolve issue #83
|
|
||||||
let oldContentOffset = collectionView.contentOffset
|
|
||||||
collectionView.collectionViewLayout.invalidateLayout()
|
|
||||||
collectionView.layoutIfNeeded()
|
|
||||||
collectionView.contentOffset = oldContentOffset
|
|
||||||
|
|
||||||
// bind input data
|
|
||||||
attribute.composeContent.value = text
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
attribute.isContentWarningComposing
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak cell, weak collectionView] isContentWarningComposing in
|
|
||||||
guard let cell = cell else { return }
|
|
||||||
guard let collectionView = collectionView else { return }
|
|
||||||
// self size input cell
|
|
||||||
collectionView.collectionViewLayout.invalidateLayout()
|
|
||||||
cell.statusContentWarningEditorView.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 { [weak collectionView] text in
|
|
||||||
guard let collectionView = collectionView else { return }
|
|
||||||
// self size input cell
|
|
||||||
collectionView.collectionViewLayout.invalidateLayout()
|
|
||||||
// bind input data
|
|
||||||
attribute.contentWarningContent.value = text
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag)
|
|
||||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
|
||||||
|
|
||||||
return cell
|
|
||||||
case .attachment(let attachmentService):
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
|
||||||
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
|
|
||||||
cell.delegate = composeStatusAttachmentTableViewCellDelegate
|
|
||||||
attachmentService.thumbnailImage
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak cell] thumbnailImage in
|
|
||||||
guard let cell = cell else { return }
|
|
||||||
let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
|
|
||||||
guard let image = thumbnailImage else {
|
|
||||||
let placeholder = UIImage.placeholder(
|
|
||||||
size: size,
|
|
||||||
color: Asset.Colors.Background.systemGroupedBackground.color
|
|
||||||
)
|
|
||||||
.af.imageRounded(
|
|
||||||
withCornerRadius: AttachmentContainerView.containerViewCornerRadius
|
|
||||||
)
|
|
||||||
cell.attachmentContainerView.previewImageView.image = placeholder
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cell.attachmentContainerView.previewImageView.image = image
|
|
||||||
.af.imageAspectScaled(toFill: size)
|
|
||||||
.af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius)
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
Publishers.CombineLatest(
|
|
||||||
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
|
|
||||||
attachmentService.error.eraseToAnyPublisher()
|
|
||||||
)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak cell, weak attachmentService] uploadState, error in
|
|
||||||
guard let cell = cell else { return }
|
|
||||||
guard let attachmentService = attachmentService else { return }
|
|
||||||
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
|
|
||||||
cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
|
|
||||||
if let error = error {
|
|
||||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
|
||||||
cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
|
|
||||||
} else {
|
|
||||||
guard let uploadState = uploadState else { return }
|
|
||||||
switch uploadState {
|
|
||||||
case is MastodonAttachmentService.UploadState.Finish,
|
|
||||||
is MastodonAttachmentService.UploadState.Fail:
|
|
||||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
|
||||||
cell.attachmentContainerView.emptyStateView.label.text = {
|
|
||||||
if let file = attachmentService.file.value {
|
|
||||||
switch file {
|
|
||||||
case .jpeg, .png, .gif:
|
|
||||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
|
||||||
case .other:
|
|
||||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
NotificationCenter.default.publisher(
|
|
||||||
for: UITextView.textDidChangeNotification,
|
|
||||||
object: cell.attachmentContainerView.descriptionTextView
|
|
||||||
)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { notification in
|
|
||||||
guard let textField = notification.object as? UITextView else { return }
|
|
||||||
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
attachmentService.description.value = text
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
return cell
|
|
||||||
case .pollOption(let attribute):
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
|
|
||||||
cell.pollOptionView.optionTextField.text = attribute.option.value
|
|
||||||
cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
|
|
||||||
cell.pollOption
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.assign(to: \.value, on: attribute.option)
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
|
|
||||||
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
|
|
||||||
return cell
|
|
||||||
case .pollOptionAppendEntry:
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
|
|
||||||
cell.delegate = composeStatusNewPollOptionCollectionViewCellDelegate
|
|
||||||
return cell
|
|
||||||
case .pollExpiresOption(let attribute):
|
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell
|
|
||||||
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal)
|
|
||||||
attribute.expiresOption
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak cell] expiresOption in
|
|
||||||
guard let cell = cell else { return }
|
|
||||||
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal)
|
|
||||||
}
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
cell.delegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
|
||||||
|
|
||||||
static func configureStatusContent(
|
static func configureStatusContent(
|
||||||
cell: ComposeStatusContentCollectionViewCell,
|
cell: ComposeStatusContentTableViewCell,
|
||||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||||
) {
|
) {
|
||||||
// set avatar
|
// set avatar
|
||||||
|
@ -275,12 +53,6 @@ extension ComposeStatusSection {
|
||||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
// bind compose content
|
|
||||||
cell.composeContent
|
|
||||||
.map { $0 as String? }
|
|
||||||
.assign(to: \.value, on: attribute.composeContent)
|
|
||||||
.store(in: &cell.disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -303,16 +75,6 @@ class CustomEmojiReplaceableTextInputReference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TextEditorView: CustomEmojiReplaceableTextInput {
|
|
||||||
func insertText(_ text: String) {
|
|
||||||
try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var isFirstResponder: Bool {
|
|
||||||
return isEditing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
extension UITextField: CustomEmojiReplaceableTextInput { }
|
extension UITextField: CustomEmojiReplaceableTextInput { }
|
||||||
extension UITextView: CustomEmojiReplaceableTextInput { }
|
extension UITextView: CustomEmojiReplaceableTextInput { }
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Kingfisher
|
import Nuke
|
||||||
|
|
||||||
enum CustomEmojiPickerSection: Equatable, Hashable {
|
enum CustomEmojiPickerSection: Equatable, Hashable {
|
||||||
case emoji(name: String)
|
case emoji(name: String)
|
||||||
|
@ -24,13 +24,13 @@ extension CustomEmojiPickerSection {
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
|
||||||
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
|
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
|
||||||
.af.imageRounded(withCornerRadius: 4)
|
.af.imageRounded(withCornerRadius: 4)
|
||||||
cell.emojiImageView.kf.setImage(
|
cell.imageTask = Nuke.loadImage(
|
||||||
with: URL(string: attribute.emoji.url),
|
with: attribute.emoji.url,
|
||||||
placeholder: placeholder,
|
options: .init(
|
||||||
options: [
|
placeholder: placeholder,
|
||||||
.transition(.fade(0.2))
|
transition: .fadeIn(duration: 0.2)
|
||||||
],
|
),
|
||||||
completionHandler: nil
|
into: cell.emojiImageView
|
||||||
)
|
)
|
||||||
cell.accessibilityLabel = attribute.emoji.shortcode
|
cell.accessibilityLabel = attribute.emoji.shortcode
|
||||||
return cell
|
return cell
|
||||||
|
@ -48,7 +48,7 @@ extension CustomEmojiPickerSection {
|
||||||
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
|
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
|
||||||
switch section {
|
switch section {
|
||||||
case .emoji(let name):
|
case .emoji(let name):
|
||||||
header.titlelabel.text = name
|
header.titleLabel.text = name
|
||||||
}
|
}
|
||||||
return header
|
return header
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -12,7 +12,9 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVKit
|
import AVKit
|
||||||
import Nuke
|
import Nuke
|
||||||
import LinkPresentation
|
import MastodonMeta
|
||||||
|
|
||||||
|
// import LinkPresentation
|
||||||
|
|
||||||
#if ASDK
|
#if ASDK
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
|
@ -138,12 +140,15 @@ extension StatusSection {
|
||||||
cell.delegate = statusTableViewCellDelegate
|
cell.delegate = statusTableViewCellDelegate
|
||||||
switch item {
|
switch item {
|
||||||
case .root:
|
case .root:
|
||||||
cell.statusView.activeTextLabel.isAccessibilityElement = false
|
// enable selection only for root
|
||||||
|
cell.statusView.contentMetaText.textView.isSelectable = true
|
||||||
|
cell.statusView.contentMetaText.textView.isAccessibilityElement = false
|
||||||
var accessibilityElements: [Any] = []
|
var accessibilityElements: [Any] = []
|
||||||
accessibilityElements.append(cell.statusView.avatarView)
|
accessibilityElements.append(cell.statusView.avatarView)
|
||||||
accessibilityElements.append(cell.statusView.nameLabel)
|
accessibilityElements.append(cell.statusView.nameLabel)
|
||||||
accessibilityElements.append(cell.statusView.dateLabel)
|
accessibilityElements.append(cell.statusView.dateLabel)
|
||||||
accessibilityElements.append(contentsOf: cell.statusView.activeTextLabel.createAccessibilityElements())
|
// TODO: a11y
|
||||||
|
accessibilityElements.append(cell.statusView.contentMetaText.textView)
|
||||||
accessibilityElements.append(contentsOf: cell.statusView.statusMosaicImageViewContainer.imageViews)
|
accessibilityElements.append(contentsOf: cell.statusView.statusMosaicImageViewContainer.imageViews)
|
||||||
accessibilityElements.append(cell.statusView.playerContainerView)
|
accessibilityElements.append(cell.statusView.playerContainerView)
|
||||||
accessibilityElements.append(cell.statusView.actionToolbarContainer)
|
accessibilityElements.append(cell.statusView.actionToolbarContainer)
|
||||||
|
@ -554,11 +559,19 @@ extension StatusSection {
|
||||||
statusItemAttribute: Item.StatusAttribute
|
statusItemAttribute: Item.StatusAttribute
|
||||||
) {
|
) {
|
||||||
// set content
|
// set content
|
||||||
cell.statusView.activeTextLabel.configure(
|
do {
|
||||||
content: (status.reblog ?? status).content,
|
let content = MastodonContent(
|
||||||
emojiDict: (status.reblog ?? status).emojiDict
|
content: (status.reblog ?? status).content,
|
||||||
)
|
emojis: (status.reblog ?? status).emojiMeta
|
||||||
cell.statusView.activeTextLabel.accessibilityLanguage = (status.reblog ?? status).language
|
)
|
||||||
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||||
|
cell.statusView.contentMetaText.configure(content: metaContent)
|
||||||
|
} catch {
|
||||||
|
cell.statusView.contentMetaText.textView.text = " "
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.statusView.contentMetaText.textView.accessibilityLanguage = (status.reblog ?? status).language
|
||||||
|
|
||||||
// set visibility
|
// set visibility
|
||||||
if let visibility = (status.reblog ?? status).visibility {
|
if let visibility = (status.reblog ?? status).visibility {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
protocol EmojiContainer {
|
protocol EmojiContainer {
|
||||||
var emojisData: Data? { get }
|
var emojisData: Data? { get }
|
||||||
|
@ -31,6 +32,14 @@ extension EmojiContainer {
|
||||||
}
|
}
|
||||||
return dict
|
return dict
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emojiMeta: MastodonContent.Emojis {
|
||||||
|
var dict = MastodonContent.Emojis()
|
||||||
|
for emoji in emojis ?? [] {
|
||||||
|
dict[emoji.shortcode] = emoji.url
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
import Meta
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
// MARK: - StatusViewDelegate
|
// MARK: - StatusViewDelegate
|
||||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
|
@ -27,6 +29,10 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, cell: cell, activeLabel: activeLabel, didTapEntity: entity)
|
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, cell: cell, activeLabel: activeLabel, didTapEntity: entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||||
|
StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta)
|
||||||
|
}
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
|
||||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
||||||
|
|
|
@ -12,6 +12,8 @@ import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
import Meta
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
#if ASDK
|
#if ASDK
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
|
@ -149,6 +151,31 @@ extension StatusProviderFacade {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func responseToStatusMetaTextAction(provider: StatusProvider, cell: UITableViewCell, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||||
|
switch meta {
|
||||||
|
case .url(_, _, let url, _):
|
||||||
|
guard let url = URL(string: url) else { return }
|
||||||
|
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
|
||||||
|
url.pathComponents.count >= 4,
|
||||||
|
url.pathComponents[0] == "/",
|
||||||
|
url.pathComponents[1] == "web",
|
||||||
|
url.pathComponents[2] == "statuses" {
|
||||||
|
let statusID = url.pathComponents[3]
|
||||||
|
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
|
||||||
|
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||||
|
} else {
|
||||||
|
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||||
|
}
|
||||||
|
case .hashtag(_, let hashtag, _):
|
||||||
|
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag)
|
||||||
|
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
|
||||||
|
case .mention(_, let mention, _):
|
||||||
|
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: mention)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if ASDK
|
#if ASDK
|
||||||
static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) {
|
static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) {
|
||||||
switch type {
|
switch type {
|
||||||
|
|
|
@ -38,7 +38,9 @@ final class AutoCompleteViewController: UIViewController {
|
||||||
tableView.backgroundColor = .clear
|
tableView.backgroundColor = .clear
|
||||||
tableView.contentInset.top = AutoCompleteViewController.chevronViewHeight
|
tableView.contentInset.top = AutoCompleteViewController.chevronViewHeight
|
||||||
tableView.verticalScrollIndicatorInsets.top = AutoCompleteViewController.chevronViewHeight
|
tableView.verticalScrollIndicatorInsets.top = AutoCompleteViewController.chevronViewHeight
|
||||||
tableView.showsVerticalScrollIndicator = false // avoid duplicate to the compose collection view indicator
|
tableView.showsVerticalScrollIndicator = false // avoid duplicate to the compose collection view indicator
|
||||||
|
tableView.preservesSuperviewLayoutMargins = false
|
||||||
|
tableView.cellLayoutMarginsFollowReadableWidth = false
|
||||||
return tableView
|
return tableView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -50,6 +52,9 @@ extension AutoCompleteViewController {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
view.backgroundColor = .clear
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
// we hack the view hierarchy. Do not preserve from superview
|
||||||
|
view.preservesSuperviewLayoutMargins = false
|
||||||
|
|
||||||
chevronView.translatesAutoresizingMaskIntoConstraints = false
|
chevronView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(chevronView)
|
view.addSubview(chevronView)
|
||||||
|
|
|
@ -97,8 +97,8 @@ extension AutoCompleteTableViewCell {
|
||||||
contentView.addSubview(containerStackView)
|
contentView.addSubview(containerStackView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
containerStackView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
containerStackView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
|
||||||
containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
//
|
|
||||||
// ComposeStatusContentCollectionViewCell.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
|
||||||
//
|
|
||||||
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
import TwitterTextEditor
|
|
||||||
|
|
||||||
final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
let statusView = StatusView()
|
|
||||||
|
|
||||||
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
|
||||||
|
|
||||||
let textEditorViewContainerView = UIView()
|
|
||||||
let textEditorView: TextEditorView = {
|
|
||||||
let textEditorView = TextEditorView()
|
|
||||||
textEditorView.font = .preferredFont(forTextStyle: .body)
|
|
||||||
textEditorView.scrollView.isScrollEnabled = false
|
|
||||||
textEditorView.isScrollEnabled = false
|
|
||||||
textEditorView.placeholderText = L10n.Scene.Compose.contentInputPlaceholder
|
|
||||||
textEditorView.keyboardType = .twitter
|
|
||||||
return textEditorView
|
|
||||||
}()
|
|
||||||
|
|
||||||
// input
|
|
||||||
weak var textEditorViewChangeObserver: TextEditorViewChangeObserver?
|
|
||||||
|
|
||||||
// output
|
|
||||||
let composeContent = PassthroughSubject<String, Never>()
|
|
||||||
let contentWarningContent = PassthroughSubject<String, Never>()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ComposeStatusContentCollectionViewCell {
|
|
||||||
|
|
||||||
private func _init() {
|
|
||||||
// selectionStyle = .none
|
|
||||||
layer.zPosition = 999
|
|
||||||
preservesSuperviewLayoutMargins = true
|
|
||||||
|
|
||||||
statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(statusContentWarningEditorView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
statusContentWarningEditorView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
||||||
statusContentWarningEditorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
||||||
statusContentWarningEditorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
||||||
])
|
|
||||||
statusContentWarningEditorView.preservesSuperviewLayoutMargins = true
|
|
||||||
statusContentWarningEditorView.containerBackgroundView.isHidden = false
|
|
||||||
|
|
||||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(statusView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
statusView.topAnchor.constraint(equalTo: statusContentWarningEditorView.bottomAnchor, constant: 20),
|
|
||||||
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
|
||||||
statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
|
||||||
])
|
|
||||||
statusView.statusContainerStackView.isHidden = true
|
|
||||||
statusView.actionToolbarContainer.isHidden = true
|
|
||||||
statusView.nameTrialingDotLabel.isHidden = true
|
|
||||||
statusView.dateLabel.isHidden = true
|
|
||||||
|
|
||||||
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
|
||||||
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
|
||||||
|
|
||||||
textEditorViewContainerView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(textEditorViewContainerView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
textEditorViewContainerView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
|
||||||
textEditorViewContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
||||||
textEditorViewContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
||||||
contentView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor, constant: 10),
|
|
||||||
])
|
|
||||||
textEditorViewContainerView.preservesSuperviewLayoutMargins = true
|
|
||||||
|
|
||||||
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
textEditorViewContainerView.addSubview(textEditorView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
textEditorView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor),
|
|
||||||
textEditorView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.leadingAnchor),
|
|
||||||
textEditorView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.trailingAnchor),
|
|
||||||
textEditorView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor),
|
|
||||||
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
|
||||||
])
|
|
||||||
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
|
||||||
|
|
||||||
statusContentWarningEditorView.textView.delegate = self
|
|
||||||
textEditorView.changeObserver = self
|
|
||||||
|
|
||||||
statusContentWarningEditorView.containerView.isHidden = true
|
|
||||||
statusView.revealContentWarningButton.isHidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - TextEditorViewChangeObserver
|
|
||||||
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
|
||||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
|
||||||
defer {
|
|
||||||
textEditorViewChangeObserver?.textEditorView(textEditorView, didChangeWithChangeResult: changeResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
|
|
||||||
guard changeResult.isTextChanged else { return }
|
|
||||||
composeContent.send(textEditorView.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITextViewDelegate
|
|
||||||
extension ComposeStatusContentCollectionViewCell: UITextViewDelegate {
|
|
||||||
|
|
||||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
|
||||||
// disable input line break
|
|
||||||
guard text != "\n" else { return false }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textView.text)
|
|
||||||
guard textView === statusContentWarningEditorView.textView else { return }
|
|
||||||
// replace line break with space
|
|
||||||
textView.text = textView.text.replacingOccurrences(of: "\n", with: " ")
|
|
||||||
contentWarningContent.send(textView.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject {
|
protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject {
|
||||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption)
|
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
|
final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
|
||||||
|
@ -41,7 +41,7 @@ final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCe
|
||||||
|
|
||||||
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||||
|
|
||||||
private typealias ExpiresOption = ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption
|
private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
durationButton.translatesAutoresizingMaskIntoConstraints = false
|
durationButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -9,7 +9,7 @@ import UIKit
|
||||||
|
|
||||||
final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableView {
|
final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableView {
|
||||||
|
|
||||||
let titlelabel: UILabel = {
|
let titleLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .bold))
|
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .bold))
|
||||||
label.textColor = Asset.Colors.Label.secondary.color
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
@ -30,13 +30,13 @@ final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableV
|
||||||
|
|
||||||
extension CustomEmojiPickerHeaderCollectionReusableView {
|
extension CustomEmojiPickerHeaderCollectionReusableView {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
titlelabel.translatesAutoresizingMaskIntoConstraints = false
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(titlelabel)
|
addSubview(titleLabel)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
titlelabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
||||||
titlelabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
titlelabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
titleLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
titlelabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,13 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Nuke
|
||||||
|
|
||||||
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
|
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
static let itemSize = CGSize(width: 44, height: 44)
|
static let itemSize = CGSize(width: 44, height: 44)
|
||||||
|
|
||||||
|
var imageTask: ImageTask?
|
||||||
|
|
||||||
let emojiImageView: UIImageView = {
|
let emojiImageView: UIImageView = {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
|
@ -23,6 +26,12 @@ final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
|
||||||
emojiImageView.alpha = isHighlighted ? 0.5 : 1.0
|
emojiImageView.alpha = isHighlighted ? 0.5 : 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
imageTask?.cancel()
|
||||||
|
imageTask = nil
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
|
@ -12,6 +12,9 @@ import PhotosUI
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
|
import MetaTextView
|
||||||
|
import MastodonMeta
|
||||||
|
import Meta
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
@ -22,7 +25,9 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
var viewModel: ComposeViewModel!
|
var viewModel: ComposeViewModel!
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "ComposeViewController", category: "logic")
|
||||||
|
|
||||||
private var suffixedAttachmentViews: [UIView] = []
|
private var suffixedAttachmentViews: [UIView] = []
|
||||||
|
|
||||||
let publishButton: UIButton = {
|
let publishButton: UIButton = {
|
||||||
|
@ -43,20 +48,17 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let collectionView: ComposeCollectionView = {
|
let tableView: ComposeTableView = {
|
||||||
let collectionViewLayout = ComposeViewController.createLayout()
|
let tableView = ComposeTableView()
|
||||||
let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self))
|
||||||
collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self))
|
tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self))
|
||||||
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
|
||||||
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
tableView.backgroundColor = Asset.Scene.Compose.background.color
|
||||||
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
|
tableView.alwaysBounceVertical = true
|
||||||
collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
|
tableView.separatorStyle = .none
|
||||||
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
tableView.tableFooterView = UIView()
|
||||||
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
return tableView
|
||||||
collectionView.alwaysBounceVertical = true
|
|
||||||
collectionView.keyboardDismissMode = .onDrag
|
|
||||||
return collectionView
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var systemKeyboardHeight: CGFloat = .zero {
|
var systemKeyboardHeight: CGFloat = .zero {
|
||||||
|
@ -148,14 +150,15 @@ extension ComposeViewController {
|
||||||
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
navigationItem.leftBarButtonItem = cancelBarButtonItem
|
||||||
navigationItem.rightBarButtonItem = publishBarButtonItem
|
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||||
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
view.addSubview(collectionView)
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -178,21 +181,31 @@ extension ComposeViewController {
|
||||||
composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
|
composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
|
||||||
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
|
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
collectionView.delegate = self
|
tableView.delegate = self
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
for: collectionView,
|
tableView: tableView,
|
||||||
dependency: self,
|
metaTextDelegate: self,
|
||||||
|
metaTextViewDelegate: self,
|
||||||
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: self,
|
composeStatusAttachmentCollectionViewCellDelegate: self,
|
||||||
textEditorViewChangeObserver: self,
|
|
||||||
composeStatusAttachmentTableViewCellDelegate: self,
|
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: self,
|
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self,
|
||||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: self
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: self
|
||||||
)
|
)
|
||||||
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
|
||||||
collectionView.addGestureRecognizer(longPressReorderGesture)
|
viewModel.composeStatusAttribute.composeContent
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard self.view.window != nil else { return }
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.beginUpdates()
|
||||||
|
self.tableView.endUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
customEmojiPickerInputView.collectionView.delegate = self
|
customEmojiPickerInputView.collectionView.delegate = self
|
||||||
viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
|
viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
|
||||||
|
@ -202,6 +215,7 @@ extension ComposeViewController {
|
||||||
)
|
)
|
||||||
|
|
||||||
// update layout when keyboard show/dismiss
|
// update layout when keyboard show/dismiss
|
||||||
|
view.layoutIfNeeded()
|
||||||
let keyboardEventPublishers = Publishers.CombineLatest3(
|
let keyboardEventPublishers = Publishers.CombineLatest3(
|
||||||
KeyboardResponderService.shared.isShow,
|
KeyboardResponderService.shared.isShow,
|
||||||
KeyboardResponderService.shared.state,
|
KeyboardResponderService.shared.state,
|
||||||
|
@ -227,8 +241,8 @@ extension ComposeViewController {
|
||||||
// update keyboard background color
|
// update keyboard background color
|
||||||
|
|
||||||
guard isShow, state == .dock else {
|
guard isShow, state == .dock else {
|
||||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.tableView.contentInset.bottom = extraMargin
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin
|
||||||
|
|
||||||
if let superView = self.autoCompleteViewController.tableView.superview {
|
if let superView = self.autoCompleteViewController.tableView.superview {
|
||||||
let autoCompleteTableViewBottomInset: CGFloat = {
|
let autoCompleteTableViewBottomInset: CGFloat = {
|
||||||
|
@ -263,18 +277,18 @@ extension ComposeViewController {
|
||||||
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
|
self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
|
||||||
|
|
||||||
// adjust inset for collectionView
|
// adjust inset for collectionView
|
||||||
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
|
||||||
let padding = contentFrame.maxY + extraMargin - endFrame.minY
|
let padding = contentFrame.maxY + extraMargin - endFrame.minY
|
||||||
guard padding > 0 else {
|
guard padding > 0 else {
|
||||||
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
|
|
||||||
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.collectionView.contentInset.bottom = padding
|
self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = padding
|
self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
|
self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
|
@ -292,15 +306,16 @@ extension ComposeViewController {
|
||||||
if self.autoCompleteViewController.view.superview == nil {
|
if self.autoCompleteViewController.view.superview == nil {
|
||||||
self.autoCompleteViewController.view.frame = self.view.bounds
|
self.autoCompleteViewController.view.frame = self.view.bounds
|
||||||
// add to container view. seealso: `viewDidLayoutSubviews()`
|
// add to container view. seealso: `viewDidLayoutSubviews()`
|
||||||
textEditorView.superview!.addSubview(self.autoCompleteViewController.view)
|
self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view)
|
||||||
self.addChild(self.autoCompleteViewController)
|
self.addChild(self.autoCompleteViewController)
|
||||||
self.autoCompleteViewController.didMove(toParent: self)
|
self.autoCompleteViewController.didMove(toParent: self)
|
||||||
self.autoCompleteViewController.view.isHidden = true
|
self.autoCompleteViewController.view.isHidden = true
|
||||||
self.collectionView.autoCompleteViewController = self.autoCompleteViewController
|
self.tableView.autoCompleteViewController = self.autoCompleteViewController
|
||||||
}
|
}
|
||||||
|
self.updateAutoCompleteViewControllerLayout()
|
||||||
self.autoCompleteViewController.view.isHidden = info == nil
|
self.autoCompleteViewController.view.isHidden = info == nil
|
||||||
guard let info = info else { return }
|
guard let info = info else { return }
|
||||||
let symbolBoundingRectInContainer = textEditorView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
|
let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
|
||||||
self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
|
self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
|
||||||
self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
|
self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
|
||||||
self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
|
self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
|
||||||
|
@ -414,8 +429,8 @@ extension ComposeViewController {
|
||||||
|
|
||||||
// setup snap behavior
|
// setup snap behavior
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
viewModel.repliedToCellFrame.removeDuplicates().eraseToAnyPublisher(),
|
viewModel.repliedToCellFrame,
|
||||||
viewModel.collectionViewState.eraseToAnyPublisher()
|
viewModel.collectionViewState
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] repliedToCellFrame, collectionViewState in
|
.sink { [weak self] repliedToCellFrame, collectionViewState in
|
||||||
|
@ -423,9 +438,11 @@ extension ComposeViewController {
|
||||||
guard repliedToCellFrame != .zero else { return }
|
guard repliedToCellFrame != .zero else { return }
|
||||||
switch collectionViewState {
|
switch collectionViewState {
|
||||||
case .fold:
|
case .fold:
|
||||||
self.collectionView.contentInset.top = -repliedToCellFrame.height
|
self.tableView.contentInset.top = -repliedToCellFrame.height
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description)
|
||||||
|
|
||||||
case .expand:
|
case .expand:
|
||||||
self.collectionView.contentInset.top = 0
|
self.tableView.contentInset.top = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -433,12 +450,21 @@ extension ComposeViewController {
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
// Fix AutoLayout conflict issue
|
// using index to make table view layout
|
||||||
DispatchQueue.main.async { [weak self] in
|
// otherwise, the content offset will be wrong
|
||||||
guard let self = self else { return }
|
guard let indexPath = tableView.indexPath(for: viewModel.composeStatusContentTableViewCell),
|
||||||
self.markTextEditorViewBecomeFirstResponser()
|
let cell = tableView.cellForRow(at: indexPath) as? ComposeStatusContentTableViewCell else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
cell.metaText.textView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
viewModel.isViewAppeared = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
@ -449,13 +475,17 @@ extension ComposeViewController {
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
updateAutoCompleteViewControllerLayout()
|
||||||
// pin autoCompleteViewController frame to window
|
}
|
||||||
|
|
||||||
|
func updateAutoCompleteViewControllerLayout() {
|
||||||
|
// pin autoCompleteViewController frame to current view
|
||||||
if let containerView = autoCompleteViewController.view.superview {
|
if let containerView = autoCompleteViewController.view.superview {
|
||||||
let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: nil)
|
let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view)
|
||||||
if viewFrameInWindow.origin.x != 0 {
|
if viewFrameInWindow.origin.x != 0 {
|
||||||
autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
|
autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
|
||||||
}
|
}
|
||||||
|
autoCompleteViewController.view.frame.size.width = view.frame.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,86 +493,56 @@ extension ComposeViewController {
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
|
||||||
private func textEditorView() -> TextEditorView? {
|
private func textEditorView() -> MetaText? {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
return viewModel.composeStatusContentTableViewCell.metaText
|
||||||
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.textEditorView
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func markTextEditorViewBecomeFirstResponser() {
|
private func markTextEditorViewBecomeFirstResponser() {
|
||||||
textEditorView()?.isEditing = true
|
textEditorView()?.textView.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func contentWarningEditorTextView() -> UITextView? {
|
private func contentWarningEditorTextView() -> UITextView? {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView
|
||||||
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? {
|
private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
guard case .pollOption = item else { return nil }
|
guard case .pollOption = item else { return nil }
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
|
||||||
guard let indexPath = diffableDataSource.indexPath(for: item),
|
guard let indexPath = dataSource.indexPath(for: item),
|
||||||
let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
|
||||||
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
|
||||||
let firstPollItem = items.first { item -> Bool in
|
let firstPollItem = items.first { item -> Bool in
|
||||||
guard case .pollOption = item else { return false }
|
guard case .pollOption = item else { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let item = firstPollItem else {
|
guard let item = firstPollItem else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return pollOptionCollectionViewCell(of: item)
|
return pollOptionCollectionViewCell(of: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
|
||||||
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
|
||||||
let lastPollItem = items.last { item -> Bool in
|
let lastPollItem = items.last { item -> Bool in
|
||||||
guard case .pollOption = item else { return false }
|
guard case .pollOption = item else { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let item = lastPollItem else {
|
guard let item = lastPollItem else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return pollOptionCollectionViewCell(of: item)
|
return pollOptionCollectionViewCell(of: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -631,43 +631,142 @@ extension ComposeViewController {
|
||||||
dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// seealso: ComposeViewModel.setupDiffableDataSource(…)
|
}
|
||||||
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
|
||||||
switch(sender.state) {
|
|
||||||
case .began:
|
|
||||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
|
||||||
let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// check if pressing reorder bar no not
|
|
||||||
let locationInCell = sender.location(in: cell)
|
|
||||||
guard cell.reorderBarImageView.frame.contains(locationInCell) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
|
||||||
case .changed:
|
|
||||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
|
||||||
let diffableDataSource = viewModel.diffableDataSource else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
|
||||||
case .pollOption = item else {
|
|
||||||
collectionView.cancelInteractiveMovement()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var position = sender.location(in: collectionView)
|
// MARK: - MetaTextDelegate
|
||||||
position.x = collectionView.frame.width * 0.5
|
extension ComposeViewController: MetaTextDelegate {
|
||||||
collectionView.updateInteractiveMovementTargetPosition(position)
|
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
|
||||||
case .ended:
|
let string = metaText.textStorage.string
|
||||||
collectionView.endInteractiveMovement()
|
let content = MastodonContent(
|
||||||
collectionView.reloadData()
|
content: string,
|
||||||
default:
|
emojis: viewModel.customEmojiViewModel.value?.emojiMapping.value ?? [:]
|
||||||
collectionView.cancelInteractiveMovement()
|
)
|
||||||
|
let metaContent = MastodonMetaContent.convert(text: content)
|
||||||
|
return metaContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension ComposeViewController: UITextViewDelegate {
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
if textEditorView()?.textView === textView {
|
||||||
|
// update model
|
||||||
|
guard let metaText = textEditorView() else { return }
|
||||||
|
let backedString = metaText.backedString
|
||||||
|
viewModel.composeStatusAttribute.composeContent.value = backedString
|
||||||
|
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
|
||||||
|
|
||||||
|
// configure auto completion
|
||||||
|
setupAutoComplete(for: textView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AutoCompleteInfo {
|
||||||
|
// model
|
||||||
|
let inputText: Substring
|
||||||
|
// range
|
||||||
|
let symbolRange: Range<String.Index>
|
||||||
|
let symbolString: Substring
|
||||||
|
let toCursorRange: Range<String.Index>
|
||||||
|
let toCursorString: Substring
|
||||||
|
let toHighlightEndRange: Range<String.Index>
|
||||||
|
let toHighlightEndString: Substring
|
||||||
|
// geometry
|
||||||
|
var textBoundingRect: CGRect = .zero
|
||||||
|
var symbolBoundingRect: CGRect = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupAutoComplete(for textView: UITextView) {
|
||||||
|
guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else {
|
||||||
|
viewModel.autoCompleteInfo.value = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
|
||||||
|
|
||||||
|
// get layout text bounding rect
|
||||||
|
var glyphRange = NSRange()
|
||||||
|
textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let textContainer = textView.layoutManager.textContainers[0]
|
||||||
|
let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
|
||||||
|
let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes.value
|
||||||
|
guard textBoundingRect.size != .zero else {
|
||||||
|
viewModel.autoCompleteRetryLayoutTimes.value += 1
|
||||||
|
// avoid infinite loop
|
||||||
|
guard retryLayoutTimes < 3 else { return }
|
||||||
|
// needs retry calculate layout when the rect position changing
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setupAutoComplete(for: textView)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModel.autoCompleteRetryLayoutTimes.value = 0
|
||||||
|
|
||||||
|
// get symbol bounding rect
|
||||||
|
textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
|
||||||
|
// set bounding rect and trigger layout
|
||||||
|
autoCompletion.textBoundingRect = textBoundingRect
|
||||||
|
autoCompletion.symbolBoundingRect = symbolBoundingRect
|
||||||
|
viewModel.autoCompleteInfo.value = autoCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? {
|
||||||
|
guard let text = textView.text,
|
||||||
|
textView.selectedRange.location > 0, !text.isEmpty,
|
||||||
|
let selectedRange = Range(textView.selectedRange, in: text) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let cursorIndex = selectedRange.upperBound
|
||||||
|
let _highlightStartIndex: String.Index? = {
|
||||||
|
var index = text.index(before: cursorIndex)
|
||||||
|
while index > text.startIndex {
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" || char == ":" {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index = text.index(before: index)
|
||||||
|
}
|
||||||
|
assert(index == text.startIndex)
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" || char == ":" {
|
||||||
|
return index
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let highlightStartIndex = _highlightStartIndex else { return nil }
|
||||||
|
let scanRange = NSRange(highlightStartIndex..<text.endIndex, in: text)
|
||||||
|
|
||||||
|
guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
|
||||||
|
guard let matchRange = Range(match.range(at: 0), in: text) else { return nil }
|
||||||
|
let matchStartIndex = matchRange.lowerBound
|
||||||
|
let matchEndIndex = matchRange.upperBound
|
||||||
|
|
||||||
|
guard matchStartIndex == highlightStartIndex, matchEndIndex >= cursorIndex else { return nil }
|
||||||
|
let symbolRange = highlightStartIndex..<text.index(after: highlightStartIndex)
|
||||||
|
let symbolString = text[symbolRange]
|
||||||
|
let toCursorRange = highlightStartIndex..<cursorIndex
|
||||||
|
let toCursorString = text[toCursorRange]
|
||||||
|
let toHighlightEndRange = matchStartIndex..<matchEndIndex
|
||||||
|
let toHighlightEndString = text[toHighlightEndRange]
|
||||||
|
|
||||||
|
let inputText = toHighlightEndString
|
||||||
|
let autoCompleteInfo = AutoCompleteInfo(
|
||||||
|
inputText: inputText,
|
||||||
|
symbolRange: symbolRange,
|
||||||
|
symbolString: symbolString,
|
||||||
|
toCursorRange: toCursorRange,
|
||||||
|
toCursorString: toCursorString,
|
||||||
|
toHighlightEndRange: toHighlightEndRange,
|
||||||
|
toHighlightEndString: toHighlightEndString
|
||||||
|
)
|
||||||
|
return autoCompleteInfo
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - TextEditorViewTextAttributesDelegate
|
// MARK: - TextEditorViewTextAttributesDelegate
|
||||||
|
@ -700,7 +799,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
}
|
}
|
||||||
self.suffixedAttachmentViews.removeAll()
|
self.suffixedAttachmentViews.removeAll()
|
||||||
|
|
||||||
// set normal apperance
|
// set normal appearance
|
||||||
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||||
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
|
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
|
||||||
attributedString.removeAttribute(.underlineStyle, range: stringRange)
|
attributedString.removeAttribute(.underlineStyle, range: stringRange)
|
||||||
|
@ -811,117 +910,6 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - TextEditorViewChangeObserver
|
|
||||||
extension ComposeViewController: TextEditorViewChangeObserver {
|
|
||||||
|
|
||||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
|
||||||
guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textEditorView: textEditorView) else {
|
|
||||||
viewModel.autoCompleteInfo.value = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
|
|
||||||
|
|
||||||
// get layout text bounding rect
|
|
||||||
var glyphRange = NSRange()
|
|
||||||
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
|
|
||||||
let textContainer = textEditorView.layoutManager.textContainers[0]
|
|
||||||
let textBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
||||||
|
|
||||||
let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes.value
|
|
||||||
guard textBoundingRect.size != .zero else {
|
|
||||||
viewModel.autoCompleteRetryLayoutTimes.value += 1
|
|
||||||
// avoid infinite loop
|
|
||||||
guard retryLayoutTimes < 3 else { return }
|
|
||||||
// needs retry calculate layout when the rect position changing
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.textEditorView(textEditorView, didChangeWithChangeResult: changeResult)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModel.autoCompleteRetryLayoutTimes.value = 0
|
|
||||||
|
|
||||||
// get symbol bounding rect
|
|
||||||
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
|
|
||||||
let symbolBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
||||||
|
|
||||||
// set bounding rect and trigger layout
|
|
||||||
autoCompletion.textBoundingRect = textBoundingRect
|
|
||||||
autoCompletion.symbolBoundingRect = symbolBoundingRect
|
|
||||||
viewModel.autoCompleteInfo.value = autoCompletion
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AutoCompleteInfo {
|
|
||||||
// model
|
|
||||||
let inputText: Substring
|
|
||||||
// range
|
|
||||||
let symbolRange: Range<String.Index>
|
|
||||||
let symbolString: Substring
|
|
||||||
let toCursorRange: Range<String.Index>
|
|
||||||
let toCursorString: Substring
|
|
||||||
let toHighlightEndRange: Range<String.Index>
|
|
||||||
let toHighlightEndString: Substring
|
|
||||||
// geometry
|
|
||||||
var textBoundingRect: CGRect = .zero
|
|
||||||
var symbolBoundingRect: CGRect = .zero
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func scanAutoCompleteInfo(textEditorView: TextEditorView) -> AutoCompleteInfo? {
|
|
||||||
let text = textEditorView.text
|
|
||||||
|
|
||||||
guard textEditorView.selectedRange.location > 0, !text.isEmpty,
|
|
||||||
let selectedRange = Range(textEditorView.selectedRange, in: text) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let cursorIndex = selectedRange.upperBound
|
|
||||||
let _highlightStartIndex: String.Index? = {
|
|
||||||
var index = text.index(before: cursorIndex)
|
|
||||||
while index > text.startIndex {
|
|
||||||
let char = text[index]
|
|
||||||
if char == "@" || char == "#" || char == ":" {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
index = text.index(before: index)
|
|
||||||
}
|
|
||||||
assert(index == text.startIndex)
|
|
||||||
let char = text[index]
|
|
||||||
if char == "@" || char == "#" || char == ":" {
|
|
||||||
return index
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
guard let highlightStartIndex = _highlightStartIndex else { return nil }
|
|
||||||
let scanRange = NSRange(highlightStartIndex..<text.endIndex, in: text)
|
|
||||||
|
|
||||||
guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
|
|
||||||
guard let matchRange = Range(match.range(at: 0), in: text) else { return nil }
|
|
||||||
let matchStartIndex = matchRange.lowerBound
|
|
||||||
let matchEndIndex = matchRange.upperBound
|
|
||||||
|
|
||||||
guard matchStartIndex == highlightStartIndex, matchEndIndex >= cursorIndex else { return nil }
|
|
||||||
let symbolRange = highlightStartIndex..<text.index(after: highlightStartIndex)
|
|
||||||
let symbolString = text[symbolRange]
|
|
||||||
let toCursorRange = highlightStartIndex..<cursorIndex
|
|
||||||
let toCursorString = text[toCursorRange]
|
|
||||||
let toHighlightEndRange = matchStartIndex..<matchEndIndex
|
|
||||||
let toHighlightEndString = text[toHighlightEndRange]
|
|
||||||
|
|
||||||
let inputText = toHighlightEndString
|
|
||||||
let autoCompleteInfo = AutoCompleteInfo(
|
|
||||||
inputText: inputText,
|
|
||||||
symbolRange: symbolRange,
|
|
||||||
symbolString: symbolString,
|
|
||||||
toCursorRange: toCursorRange,
|
|
||||||
toCursorString: toCursorString,
|
|
||||||
toHighlightEndRange: toHighlightEndRange,
|
|
||||||
toHighlightEndString: toHighlightEndString
|
|
||||||
)
|
|
||||||
return autoCompleteInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - ComposeToolbarViewDelegate
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
|
@ -941,7 +929,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
// setup initial poll option if needs
|
// setup initial poll option if needs
|
||||||
if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty {
|
if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty {
|
||||||
viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()]
|
viewModel.pollOptionAttributes.value = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()]
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.isPollComposing.value {
|
if viewModel.isPollComposing.value {
|
||||||
|
@ -984,7 +972,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
// MARK: - UIScrollViewDelegate
|
// MARK: - UIScrollViewDelegate
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
guard scrollView === collectionView else { return }
|
guard scrollView === tableView else { return }
|
||||||
|
|
||||||
let repliedToCellFrame = viewModel.repliedToCellFrame.value
|
let repliedToCellFrame = viewModel.repliedToCellFrame.value
|
||||||
guard repliedToCellFrame != .zero else { return }
|
guard repliedToCellFrame != .zero else { return }
|
||||||
|
@ -1007,6 +995,9 @@ extension ComposeViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension ComposeViewController: UITableViewDelegate { }
|
||||||
|
|
||||||
// MARK: - UICollectionViewDelegate
|
// MARK: - UICollectionViewDelegate
|
||||||
extension ComposeViewController: UICollectionViewDelegate {
|
extension ComposeViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
|
@ -1018,26 +1009,13 @@ extension ComposeViewController: UICollectionViewDelegate {
|
||||||
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||||
guard case let .emoji(attribute) = item else { return }
|
guard case let .emoji(attribute) = item else { return }
|
||||||
let emoji = attribute.emoji
|
let emoji = attribute.emoji
|
||||||
let textEditorView = self.textEditorView()
|
|
||||||
|
// make click sound
|
||||||
|
UIDevice.current.playInputClick()
|
||||||
|
|
||||||
// retrieve active text input and insert emoji
|
// retrieve active text input and insert emoji
|
||||||
// the leading and trailing space is REQUIRED to fix `UITextStorage` layout issue
|
// the trailing space is REQUIRED to make regex happy
|
||||||
let reference = viewModel.customEmojiPickerInputViewModel.insertText(" :\(emoji.shortcode): ")
|
_ = 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()
|
|
||||||
|
|
||||||
// make click sound
|
|
||||||
UIDevice.current.playInputClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
@ -1124,17 +1102,17 @@ extension ComposeViewController: UIDocumentPickerDelegate {
|
||||||
extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
|
extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
|
||||||
|
|
||||||
func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
|
func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return }
|
||||||
guard let indexPath = collectionView.indexPath(for: cell) else { return }
|
guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
guard case let .attachment(attachmentService) = item else { return }
|
guard case let .attachment(attachmentService) = item else { return }
|
||||||
|
|
||||||
var attachmentServices = viewModel.attachmentServices.value
|
var attachmentServices = viewModel.attachmentServices.value
|
||||||
guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
|
guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
|
||||||
let removedItem = attachmentServices[index]
|
let removedItem = attachmentServices[index]
|
||||||
attachmentServices.remove(at: index)
|
attachmentServices.remove(at: index)
|
||||||
viewModel.attachmentServices.value = attachmentServices
|
viewModel.attachmentServices.value = attachmentServices
|
||||||
|
|
||||||
// cancel task
|
// cancel task
|
||||||
removedItem.disposeBag.removeAll()
|
removedItem.disposeBag.removeAll()
|
||||||
}
|
}
|
||||||
|
@ -1155,16 +1133,16 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega
|
||||||
// handle delete backward event for poll option input
|
// handle delete backward event for poll option input
|
||||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
||||||
guard (text ?? "").isEmpty else { return }
|
guard (text ?? "").isEmpty else { return }
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
|
||||||
guard let indexPath = collectionView.indexPath(for: cell) else { return }
|
guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
guard case let .pollOption(attribute) = item else { return }
|
guard case let .pollOption(attribute) = item else { return }
|
||||||
|
|
||||||
var pollAttributes = viewModel.pollOptionAttributes.value
|
var pollAttributes = viewModel.pollOptionAttributes.value
|
||||||
guard let index = pollAttributes.firstIndex(of: attribute) else { return }
|
guard let index = pollAttributes.firstIndex(of: attribute) else { return }
|
||||||
|
|
||||||
// mark previous (fallback to next) item of removed middle poll option become first responder
|
// mark previous (fallback to next) item of removed middle poll option become first responder
|
||||||
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main)
|
||||||
if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
|
if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
|
||||||
func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
guard index > 0 else { return nil }
|
guard index > 0 else { return nil }
|
||||||
|
@ -1172,7 +1150,7 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega
|
||||||
let itemBeforeRemoved = pollItems[indexBeforeRemoved]
|
let itemBeforeRemoved = pollItems[indexBeforeRemoved]
|
||||||
return pollOptionCollectionViewCell(of: itemBeforeRemoved)
|
return pollOptionCollectionViewCell(of: itemBeforeRemoved)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
guard index < pollItems.count - 1 else { return nil }
|
guard index < pollItems.count - 1 else { return nil }
|
||||||
let indexAfterRemoved = pollItems.index(after: index)
|
let indexAfterRemoved = pollItems.index(after: index)
|
||||||
|
@ -1186,27 +1164,27 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega
|
||||||
}
|
}
|
||||||
cell?.pollOptionView.optionTextField.becomeFirstResponder()
|
cell?.pollOptionView.optionTextField.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard pollAttributes.count > 2 else {
|
guard pollAttributes.count > 2 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pollAttributes.remove(at: index)
|
pollAttributes.remove(at: index)
|
||||||
|
|
||||||
// update data source
|
// update data source
|
||||||
viewModel.pollOptionAttributes.value = pollAttributes
|
viewModel.pollOptionAttributes.value = pollAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle keyboard return event for poll option input
|
// handle keyboard return event for poll option input
|
||||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
|
||||||
guard let indexPath = collectionView.indexPath(for: cell) else { return }
|
guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
|
||||||
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll).filter { item in
|
let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in
|
||||||
guard case .pollOption = item else { return false }
|
guard case .pollOption = item else { return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
guard let index = pollItems.firstIndex(of: item) else { return }
|
guard let index = pollItems.firstIndex(of: item) else { return }
|
||||||
|
|
||||||
if index == pollItems.count - 1 {
|
if index == pollItems.count - 1 {
|
||||||
// is the last
|
// is the last
|
||||||
viewModel.createNewPollOptionIfPossible()
|
viewModel.createNewPollOptionIfPossible()
|
||||||
|
@ -1236,7 +1214,7 @@ extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionVie
|
||||||
|
|
||||||
// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
|
extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
|
||||||
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) {
|
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) {
|
||||||
viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
|
viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1264,14 +1242,22 @@ extension ComposeViewController: AutoCompleteViewControllerDelegate {
|
||||||
}()
|
}()
|
||||||
guard let replacedText = _replacedText else { return }
|
guard let replacedText = _replacedText else { return }
|
||||||
|
|
||||||
guard let textEditorView = textEditorView() else { return }
|
guard let textEditorView = textEditorView(),
|
||||||
let text = textEditorView.text
|
let text = textEditorView.textView.text else { return }
|
||||||
|
|
||||||
do {
|
|
||||||
try textEditorView.updateByReplacing(range: NSRange(info.toHighlightEndRange, in: text), with: replacedText)
|
let range = NSRange(info.toHighlightEndRange, in: text)
|
||||||
viewModel.autoCompleteInfo.value = nil
|
textEditorView.textStorage.replaceCharacters(in: range, with: replacedText)
|
||||||
} catch {
|
viewModel.autoCompleteInfo.value = nil
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete fail %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
|
||||||
|
switch item {
|
||||||
|
case .emoji, .bottomLoader:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// set selected range except emoji
|
||||||
|
let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
|
||||||
|
guard textEditorView.textStorage.length <= newRange.location else { return }
|
||||||
|
textEditorView.textView.selectedRange = newRange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,73 +5,98 @@
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import CoreDataStack
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonMeta
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
extension ComposeViewModel {
|
extension ComposeViewModel {
|
||||||
|
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
for collectionView: UICollectionView,
|
tableView: UITableView,
|
||||||
dependency: NeedsDependency,
|
metaTextDelegate: MetaTextDelegate,
|
||||||
|
metaTextViewDelegate: UITextViewDelegate,
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
textEditorViewChangeObserver: TextEditorViewChangeObserver,
|
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
) {
|
) {
|
||||||
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
|
// content
|
||||||
for: collectionView,
|
composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
|
||||||
dependency: dependency,
|
composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
|
||||||
managedObjectContext: context.managedObjectContext,
|
// attachment
|
||||||
composeKind: composeKind,
|
composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
|
||||||
repliedToCellFrameSubscriber: repliedToCellFrame,
|
// poll
|
||||||
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
composeStatusPollTableViewCell.delegate = self
|
||||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
|
||||||
textEditorViewChangeObserver: textEditorViewChangeObserver,
|
composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
|
||||||
composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate
|
|
||||||
)
|
|
||||||
|
|
||||||
diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
// setup data source
|
||||||
switch item {
|
tableView.dataSource = self
|
||||||
case .pollOption: return true
|
|
||||||
default: return false
|
attachmentServices
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] attachmentServices in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard self.isViewAppeared else { return }
|
||||||
|
|
||||||
|
let cell = self.composeStatusAttachmentTableViewCell
|
||||||
|
guard let dataSource = cell.dataSource else { return }
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) }
|
||||||
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
|
tableView.performBatchUpdates {
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
} completion: { _ in
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// update reordered data source
|
Publishers.CombineLatest(
|
||||||
diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
isPollComposing,
|
||||||
|
pollOptionAttributes
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isPollComposing, pollOptionAttributes in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
guard self.isViewAppeared else { return }
|
||||||
let items = transaction.finalSnapshot.itemIdentifiers
|
|
||||||
var pollOptionAttributes: [ComposeStatusItem.ComposePollOptionAttribute] = []
|
let cell = self.composeStatusPollTableViewCell
|
||||||
for item in items {
|
guard let dataSource = cell.dataSource else { return }
|
||||||
guard case let .pollOption(attribute) = item else { continue }
|
|
||||||
pollOptionAttributes.append(attribute)
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
var items: [ComposeStatusPollItem] = []
|
||||||
|
if isPollComposing {
|
||||||
|
for attribute in pollOptionAttributes {
|
||||||
|
items.append(.pollOption(attribute: attribute))
|
||||||
|
}
|
||||||
|
if pollOptionAttributes.count < 4 {
|
||||||
|
items.append(.pollOptionAppendEntry)
|
||||||
|
}
|
||||||
|
items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
|
||||||
|
}
|
||||||
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
|
tableView.performBatchUpdates {
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
} completion: { _ in
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
self.pollOptionAttributes.value = pollOptionAttributes
|
|
||||||
}
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
self.diffableDataSource = diffableDataSource
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
|
||||||
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
|
||||||
switch composeKind {
|
|
||||||
case .reply(let statusObjectID):
|
|
||||||
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
|
||||||
snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo)
|
|
||||||
case .hashtag, .mention, .post:
|
|
||||||
snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status)
|
|
||||||
}
|
|
||||||
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
// some magic fix modal presentation animation issue
|
|
||||||
collectionView.dataSource = diffableDataSource
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCustomEmojiPickerDiffableDataSource(
|
func setupCustomEmojiPickerDiffableDataSource(
|
||||||
|
@ -120,3 +145,151 @@ extension ComposeViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSource
|
||||||
|
extension ComposeViewModel: UITableViewDataSource {
|
||||||
|
|
||||||
|
enum Section: CaseIterable {
|
||||||
|
case repliedTo
|
||||||
|
case status
|
||||||
|
case attachment
|
||||||
|
case poll
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
return Section.allCases.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
switch Section.allCases[section] {
|
||||||
|
case .repliedTo:
|
||||||
|
switch composeKind {
|
||||||
|
case .reply: return 1
|
||||||
|
default: return 0
|
||||||
|
}
|
||||||
|
case .status: return 1
|
||||||
|
case .attachment:
|
||||||
|
return 1
|
||||||
|
case .poll:
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
switch Section.allCases[indexPath.section] {
|
||||||
|
case .repliedTo:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
|
||||||
|
guard case let .reply(statusObjectID) = composeKind else { return cell }
|
||||||
|
cell.framePublisher
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.value, on: self.repliedToCellFrame)
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
let managedObjectContext = context.managedObjectContext
|
||||||
|
managedObjectContext.performAndWait {
|
||||||
|
guard let replyTo = managedObjectContext.object(with: statusObjectID) as? Status else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let status = replyTo.reblog ?? replyTo
|
||||||
|
|
||||||
|
// set avatar
|
||||||
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||||
|
// set name username
|
||||||
|
cell.statusView.nameLabel.configure(content: status.author.displayNameWithFallback, emojiDict: status.author.emojiDict)
|
||||||
|
cell.statusView.usernameLabel.text = "@" + status.author.acct
|
||||||
|
// set text
|
||||||
|
let content = MastodonContent(content: status.content, emojis: status.emojiMeta)
|
||||||
|
do {
|
||||||
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||||
|
cell.statusView.contentMetaText.configure(content: metaContent)
|
||||||
|
} catch {
|
||||||
|
cell.statusView.contentMetaText.textView.text = " "
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
// set date
|
||||||
|
cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
case .status:
|
||||||
|
let cell = self.composeStatusContentTableViewCell
|
||||||
|
// configure header
|
||||||
|
let managedObjectContext = context.managedObjectContext
|
||||||
|
managedObjectContext.performAndWait {
|
||||||
|
guard case let .reply(replyToStatusObjectID) = self.composeKind,
|
||||||
|
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||||
|
cell.statusView.headerContainerView.isHidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cell.statusView.headerContainerView.isHidden = false
|
||||||
|
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||||
|
let headerText: String = {
|
||||||
|
let author = replyTo.author
|
||||||
|
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||||
|
return L10n.Scene.Compose.replyingToUser(name)
|
||||||
|
}()
|
||||||
|
MastodonStatusContent.parseResult(content: headerText, emojiDict: replyTo.author.emojiDict)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell] parseResult in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
}
|
||||||
|
// configure author
|
||||||
|
ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute)
|
||||||
|
// configure content warning
|
||||||
|
cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent.value
|
||||||
|
// bind content warning
|
||||||
|
composeStatusAttribute.isContentWarningComposing
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell, weak tableView] isContentWarningComposing in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
guard let tableView = tableView else { return }
|
||||||
|
// self size input cell
|
||||||
|
cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
|
||||||
|
cell.statusContentWarningEditorView.alpha = 0
|
||||||
|
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||||
|
cell.statusContentWarningEditorView.alpha = 1
|
||||||
|
} completion: { _ in
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
|
cell.contentWarningContent
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak tableView, weak self] text in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// bind input data
|
||||||
|
self.composeStatusAttribute.contentWarningContent.value = text
|
||||||
|
|
||||||
|
// self size input cell
|
||||||
|
guard let tableView = tableView else { return }
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
// configure custom emoji picker
|
||||||
|
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag)
|
||||||
|
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag)
|
||||||
|
return cell
|
||||||
|
case .attachment:
|
||||||
|
let cell = self.composeStatusAttachmentTableViewCell
|
||||||
|
return cell
|
||||||
|
case .poll:
|
||||||
|
let cell = self.composeStatusPollTableViewCell
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ComposeStatusPollTableViewCellDelegate
|
||||||
|
extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
|
||||||
|
func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
self.pollOptionAttributes.value = options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
final class ComposeViewModel {
|
final class ComposeViewModel: NSObject {
|
||||||
|
|
||||||
static let composeContentLimit: Int = 500
|
static let composeContentLimit: Int = 500
|
||||||
|
|
||||||
|
@ -33,9 +33,14 @@ final class ComposeViewModel {
|
||||||
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
|
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
|
||||||
let autoCompleteInfo = CurrentValueSubject<ComposeViewController.AutoCompleteInfo?, Never>(nil)
|
let autoCompleteInfo = CurrentValueSubject<ComposeViewController.AutoCompleteInfo?, Never>(nil)
|
||||||
|
var isViewAppeared = false
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
|
||||||
|
let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
|
||||||
|
let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
|
||||||
|
|
||||||
|
var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>!
|
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>!
|
||||||
private(set) lazy var publishStateMachine: GKStateMachine = {
|
private(set) lazy var publishStateMachine: GKStateMachine = {
|
||||||
// exclude timeline middle fetcher state
|
// exclude timeline middle fetcher state
|
||||||
|
@ -61,7 +66,7 @@ final class ComposeViewModel {
|
||||||
let characterCount = CurrentValueSubject<Int, Never>(0)
|
let characterCount = CurrentValueSubject<Int, Never>(0)
|
||||||
let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
|
let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
|
||||||
|
|
||||||
// for hashtag: "#<hashag> "
|
// for hashtag: "#<hashtag> "
|
||||||
// for mention: "@<mention> "
|
// for mention: "@<mention> "
|
||||||
private(set) var preInsertedContent: String?
|
private(set) var preInsertedContent: String?
|
||||||
|
|
||||||
|
@ -75,8 +80,8 @@ final class ComposeViewModel {
|
||||||
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||||
|
|
||||||
// polls
|
// polls
|
||||||
let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([])
|
let pollOptionAttributes = CurrentValueSubject<[ComposeStatusPollItem.PollOptionAttribute], Never>([])
|
||||||
let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute()
|
let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
|
@ -91,7 +96,9 @@ final class ComposeViewModel {
|
||||||
self.selectedStatusVisibility = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value?.user.locked == true ? .private : .public)
|
self.selectedStatusVisibility = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value?.user.locked == true ? .private : .public)
|
||||||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||||
|
super.init()
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
switch composeKind {
|
switch composeKind {
|
||||||
case .reply(let repliedToStatusObjectID):
|
case .reply(let repliedToStatusObjectID):
|
||||||
context.managedObjectContext.performAndWait {
|
context.managedObjectContext.performAndWait {
|
||||||
|
@ -143,7 +150,7 @@ final class ComposeViewModel {
|
||||||
case .post:
|
case .post:
|
||||||
self.preInsertedContent = nil
|
self.preInsertedContent = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
isCustomEmojiComposing
|
isCustomEmojiComposing
|
||||||
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -282,45 +289,13 @@ final class ComposeViewModel {
|
||||||
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
|
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind snapshot
|
|
||||||
Publishers.CombineLatest3(
|
|
||||||
attachmentServices.eraseToAnyPublisher(),
|
|
||||||
isPollComposing.eraseToAnyPublisher(),
|
|
||||||
pollOptionAttributes.eraseToAnyPublisher()
|
|
||||||
)
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] attachmentServices, isPollComposing, pollAttributes in
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: trigger attachments upload…", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
|
|
||||||
guard let self = self else { return }
|
// setup attribute updater
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
Publishers.CombineLatest(
|
||||||
var snapshot = diffableDataSource.snapshot()
|
attachmentServices,
|
||||||
|
context.timestampUpdatePublisher
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
|
)
|
||||||
var attachmentItems: [ComposeStatusItem] = []
|
.sink { attachmentServices, _ in
|
||||||
for attachmentService in attachmentServices {
|
|
||||||
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
|
|
||||||
attachmentItems.append(item)
|
|
||||||
}
|
|
||||||
snapshot.appendItems(attachmentItems, toSection: .attachment)
|
|
||||||
|
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .poll))
|
|
||||||
if isPollComposing {
|
|
||||||
var pollItems: [ComposeStatusItem] = []
|
|
||||||
for pollAttribute in pollAttributes {
|
|
||||||
let item = ComposeStatusItem.pollOption(attribute: pollAttribute)
|
|
||||||
pollItems.append(item)
|
|
||||||
}
|
|
||||||
snapshot.appendItems(pollItems, toSection: .poll)
|
|
||||||
if pollAttributes.count < 4 {
|
|
||||||
snapshot.appendItems([ComposeStatusItem.pollOptionAppendEntry], toSection: .poll)
|
|
||||||
}
|
|
||||||
snapshot.appendItems([ComposeStatusItem.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)], toSection: .poll)
|
|
||||||
}
|
|
||||||
|
|
||||||
diffableDataSource.apply(snapshot)
|
|
||||||
|
|
||||||
// drive service upload state
|
// drive service upload state
|
||||||
// make image upload in the queue
|
// make image upload in the queue
|
||||||
for attachmentService in attachmentServices {
|
for attachmentService in attachmentServices {
|
||||||
|
@ -393,7 +368,7 @@ extension ComposeViewModel {
|
||||||
func createNewPollOptionIfPossible() {
|
func createNewPollOptionIfPossible() {
|
||||||
guard pollOptionAttributes.value.count < 4 else { return }
|
guard pollOptionAttributes.value.count < 4 else { return }
|
||||||
|
|
||||||
let attribute = ComposeStatusItem.ComposePollOptionAttribute()
|
let attribute = ComposeStatusPollItem.PollOptionAttribute()
|
||||||
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,7 +440,7 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||||
|
|
||||||
// MARK: - ComposePollAttributeDelegate
|
// MARK: - ComposePollAttributeDelegate
|
||||||
extension ComposeViewModel: ComposePollAttributeDelegate {
|
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
|
||||||
// trigger update
|
// trigger update
|
||||||
pollOptionAttributes.value = pollOptionAttributes.value
|
pollOptionAttributes.value = pollOptionAttributes.value
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,61 @@
|
||||||
//
|
//
|
||||||
// ComposeRepliedToStatusContentCollectionViewCell.swift
|
// ComposeRepliedToStatusContentTableViewCell.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
// Created by MainasuK Cirno on 2021-6-28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell {
|
final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
let statusView = StatusView()
|
let statusView = ReplicaStatusView()
|
||||||
|
|
||||||
let framePublisher = PassthroughSubject<CGRect, Never>()
|
let framePublisher = PassthroughSubject<CGRect, Never>()
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
|
||||||
disposeBag.removeAll()
|
disposeBag.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(frame: frame)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
framePublisher.send(bounds)
|
framePublisher.send(bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeRepliedToStatusContentCollectionViewCell {
|
extension ComposeRepliedToStatusContentTableViewCell {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
|
||||||
statusView.actionToolbarContainer.isHidden = true
|
|
||||||
statusView.revealContentWarningButton.isHidden = true
|
|
||||||
|
|
||||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(statusView)
|
contentView.addSubview(statusView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).identifier("statusView.top to ComposeRepliedToStatusContentCollectionViewCell.contentView.top"),
|
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).identifier("statusView.top to ComposeRepliedToStatusContentCollectionViewCell.contentView.top"),
|
||||||
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
statusView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
|
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10).identifier("ComposeRepliedToStatusContentCollectionViewCell.contentView.bottom to statusView.bottom"),
|
contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10).identifier("ComposeRepliedToStatusContentCollectionViewCell.contentView.bottom to statusView.bottom"),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
statusView.headerContainerView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusAttachmentTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
|
final class ComposeStatusAttachmentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
private(set) var dataSource: UICollectionViewDiffableDataSource<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>!
|
||||||
|
weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate?
|
||||||
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
private static func createLayout() -> UICollectionViewLayout {
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||||
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
return UICollectionViewCompositionalLayout(section: section)
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint!
|
||||||
|
let collectionView: UICollectionView = {
|
||||||
|
let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout()
|
||||||
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
|
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
||||||
|
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
||||||
|
collectionView.alwaysBounceVertical = true
|
||||||
|
collectionView.isScrollEnabled = false
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusAttachmentTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(collectionView)
|
||||||
|
collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
collectionViewHeightLayoutConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
|
collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height
|
||||||
|
}
|
||||||
|
.store(in: &observations)
|
||||||
|
|
||||||
|
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||||
|
weak self
|
||||||
|
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||||
|
guard let self = self else { return UICollectionViewCell() }
|
||||||
|
switch item {
|
||||||
|
case .attachment(let attachmentService):
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
||||||
|
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
|
||||||
|
cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate
|
||||||
|
attachmentService.thumbnailImage
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell] thumbnailImage in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
|
||||||
|
guard let image = thumbnailImage else {
|
||||||
|
let placeholder = UIImage.placeholder(
|
||||||
|
size: size,
|
||||||
|
color: Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
)
|
||||||
|
.af.imageRounded(
|
||||||
|
withCornerRadius: AttachmentContainerView.containerViewCornerRadius
|
||||||
|
)
|
||||||
|
cell.attachmentContainerView.previewImageView.image = placeholder
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cell.attachmentContainerView.previewImageView.image = image
|
||||||
|
.af.imageAspectScaled(toFill: size)
|
||||||
|
.af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
|
||||||
|
attachmentService.error.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell, weak attachmentService] uploadState, error in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
guard let attachmentService = attachmentService else { return }
|
||||||
|
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
|
||||||
|
cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
|
||||||
|
if let error = error {
|
||||||
|
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||||
|
cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
|
||||||
|
} else {
|
||||||
|
guard let uploadState = uploadState else { return }
|
||||||
|
switch uploadState {
|
||||||
|
case is MastodonAttachmentService.UploadState.Finish,
|
||||||
|
is MastodonAttachmentService.UploadState.Fail:
|
||||||
|
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||||
|
cell.attachmentContainerView.emptyStateView.label.text = {
|
||||||
|
if let file = attachmentService.file.value {
|
||||||
|
switch file {
|
||||||
|
case .jpeg, .png, .gif:
|
||||||
|
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||||
|
case .other:
|
||||||
|
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
NotificationCenter.default.publisher(
|
||||||
|
for: UITextView.textDidChangeNotification,
|
||||||
|
object: cell.attachmentContainerView.descriptionTextView
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { notification in
|
||||||
|
guard let textField = notification.object as? UITextView else { return }
|
||||||
|
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
attachmentService.description.value = text
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusContentTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
|
final class ComposeStatusContentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let statusView = ReplicaStatusView()
|
||||||
|
|
||||||
|
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
||||||
|
|
||||||
|
let textEditorViewContainerView = UIView()
|
||||||
|
|
||||||
|
static let metaTextViewTag: Int = 333
|
||||||
|
let metaText: MetaText = {
|
||||||
|
let metaText = MetaText()
|
||||||
|
metaText.textView.backgroundColor = .clear
|
||||||
|
metaText.textView.isScrollEnabled = false
|
||||||
|
metaText.textView.keyboardType = .twitter
|
||||||
|
metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset
|
||||||
|
metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||||
|
metaText.textView.attributedPlaceholder = {
|
||||||
|
var attributes = metaText.textAttributes
|
||||||
|
attributes[.foregroundColor] = Asset.Colors.Label.secondary.color
|
||||||
|
return NSAttributedString(
|
||||||
|
string: L10n.Scene.Compose.contentInputPlaceholder,
|
||||||
|
attributes: attributes
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
return metaText
|
||||||
|
}()
|
||||||
|
|
||||||
|
// output
|
||||||
|
let contentWarningContent = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
metaText.delegate = nil
|
||||||
|
metaText.textView.delegate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusContentTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
|
layer.zPosition = 999
|
||||||
|
preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
containerStackView.axis = .vertical
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
containerStackView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(statusContentWarningEditorView)
|
||||||
|
statusContentWarningEditorView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
|
let statusContainerView = UIView()
|
||||||
|
statusContainerView.preservesSuperviewLayoutMargins = true
|
||||||
|
containerStackView.addArrangedSubview(statusContainerView)
|
||||||
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
statusContainerView.addSubview(statusView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusView.topAnchor.constraint(equalTo: statusContainerView.topAnchor, constant: 20),
|
||||||
|
statusView.leadingAnchor.constraint(equalTo: statusContainerView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
statusView.trailingAnchor.constraint(equalTo: statusContainerView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
statusView.bottomAnchor.constraint(equalTo: statusContainerView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(textEditorViewContainerView)
|
||||||
|
metaText.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
textEditorViewContainerView.addSubview(metaText.textView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor),
|
||||||
|
metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor),
|
||||||
|
metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 88).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
statusContentWarningEditorView.textView.delegate = self
|
||||||
|
|
||||||
|
statusView.nameTrialingDotLabel.isHidden = true
|
||||||
|
statusView.dateLabel.isHidden = true
|
||||||
|
statusContentWarningEditorView.isHidden = true
|
||||||
|
statusView.statusContainerStackView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension ComposeStatusContentTableViewCell: UITextViewDelegate {
|
||||||
|
|
||||||
|
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
switch textView {
|
||||||
|
case statusContentWarningEditorView.textView:
|
||||||
|
// disable input line break
|
||||||
|
guard text != "\n" else { return false }
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusPollTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ComposeStatusPollTableViewCellDelegate: AnyObject {
|
||||||
|
func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute])
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComposeStatusPollTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
private(set) var dataSource: UICollectionViewDiffableDataSource<ComposeStatusPollSection, ComposeStatusPollItem>!
|
||||||
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel?
|
||||||
|
weak var delegate: ComposeStatusPollTableViewCellDelegate?
|
||||||
|
weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate?
|
||||||
|
weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate?
|
||||||
|
weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
|
||||||
|
|
||||||
|
|
||||||
|
private static func createLayout() -> UICollectionViewLayout {
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||||
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
return UICollectionViewCompositionalLayout(section: section)
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint!
|
||||||
|
let collectionView: UICollectionView = {
|
||||||
|
let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout()
|
||||||
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
|
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
|
||||||
|
collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
|
||||||
|
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
||||||
|
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
||||||
|
collectionView.alwaysBounceVertical = true
|
||||||
|
collectionView.isScrollEnabled = false
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(collectionView)
|
||||||
|
collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
collectionViewHeightLayoutConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
|
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeStatusPollTableViewCell.longPressReorderGestureHandler(_:)))
|
||||||
|
collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||||
|
|
||||||
|
collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
print(collectionView.contentSize)
|
||||||
|
self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height
|
||||||
|
}
|
||||||
|
.store(in: &observations)
|
||||||
|
|
||||||
|
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||||
|
weak self
|
||||||
|
] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||||
|
guard let self = self else { return UICollectionViewCell() }
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .pollOption(let attribute):
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
|
||||||
|
cell.pollOptionView.optionTextField.text = attribute.option.value
|
||||||
|
cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
|
||||||
|
cell.pollOption
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.value, on: attribute.option)
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate
|
||||||
|
if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel {
|
||||||
|
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
case .pollOptionAppendEntry:
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
|
||||||
|
cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||||
|
return cell
|
||||||
|
case .pollExpiresOption(let attribute):
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell
|
||||||
|
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal)
|
||||||
|
attribute.expiresOption
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak cell] expiresOption in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.reorderingHandlers.canReorderItem = { item in
|
||||||
|
switch item {
|
||||||
|
case .pollOption: return true
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update reordered data source
|
||||||
|
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let items = transaction.finalSnapshot.itemIdentifiers
|
||||||
|
var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
|
||||||
|
for item in items {
|
||||||
|
guard case let .pollOption(attribute) = item else { continue }
|
||||||
|
pollOptionAttributes.append(attribute)
|
||||||
|
}
|
||||||
|
self.delegate?.composeStatusPollTableViewCell(self, pollOptionAttributesDidReorder: pollOptionAttributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollTableViewCell {
|
||||||
|
|
||||||
|
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
||||||
|
switch(sender.state) {
|
||||||
|
case .began:
|
||||||
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||||
|
let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// check if pressing reorder bar no not
|
||||||
|
let locationInCell = sender.location(in: cell)
|
||||||
|
guard cell.reorderBarImageView.frame.contains(locationInCell) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
||||||
|
case .changed:
|
||||||
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||||
|
let dataSource = self.dataSource else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
guard let item = dataSource.itemIdentifier(for: selectedIndexPath),
|
||||||
|
case .pollOption = item else {
|
||||||
|
collectionView.cancelInteractiveMovement()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = sender.location(in: collectionView)
|
||||||
|
position.x = collectionView.frame.width * 0.5
|
||||||
|
collectionView.updateInteractiveMovementTargetPosition(position)
|
||||||
|
case .ended:
|
||||||
|
collectionView.endInteractiveMovement()
|
||||||
|
collectionView.reloadData()
|
||||||
|
default:
|
||||||
|
collectionView.cancelInteractiveMovement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,22 +1,22 @@
|
||||||
//
|
//
|
||||||
// ComposeCollectionView.swift
|
// ComposeTableView.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-5-17.
|
// Created by MainasuK Cirno on 2021-6-28.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class ComposeCollectionView: UICollectionView {
|
final class ComposeTableView: UITableView {
|
||||||
|
|
||||||
weak var autoCompleteViewController: AutoCompleteViewController?
|
weak var autoCompleteViewController: AutoCompleteViewController?
|
||||||
|
|
||||||
// adjust hitTest for auto-complete
|
// adjust hitTest for auto-complete
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
guard let autoCompleteViewController = autoCompleteViewController else {
|
guard let autoCompleteViewController = autoCompleteViewController else {
|
||||||
return super.hitTest(point, with: event)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
let thePoint = convert(point, to: autoCompleteViewController.view)
|
let thePoint = convert(point, to: autoCompleteViewController.view)
|
||||||
if let hitView = autoCompleteViewController.view.hitTest(thePoint, with: event) {
|
if let hitView = autoCompleteViewController.view.hitTest(thePoint, with: event) {
|
||||||
return hitView
|
return hitView
|
||||||
|
@ -24,5 +24,5 @@ final class ComposeCollectionView: UICollectionView {
|
||||||
return super.hitTest(point, with: event)
|
return super.hitTest(point, with: event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,266 @@
|
||||||
|
//
|
||||||
|
// ReplicaStatusView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-6-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import ActiveLabel
|
||||||
|
import FLAnimatedImage
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
|
final class ReplicaStatusView: 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 containerStackViewSpacing: CGFloat = 10
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
let headerContainerView = UIView()
|
||||||
|
let authorContainerView = UIView()
|
||||||
|
|
||||||
|
static let reblogIconImage: UIImage = {
|
||||||
|
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
|
||||||
|
let configuration = UIImage.SymbolConfiguration(font: font)
|
||||||
|
let image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
|
||||||
|
return image
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let replyIconImage: UIImage = {
|
||||||
|
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
|
||||||
|
let configuration = UIImage.SymbolConfiguration(font: font)
|
||||||
|
let image = UIImage(systemName: "arrowshape.turn.up.left.fill", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
|
||||||
|
return image
|
||||||
|
}()
|
||||||
|
|
||||||
|
static func iconAttributedString(image: UIImage) -> NSAttributedString {
|
||||||
|
let attributedString = NSMutableAttributedString()
|
||||||
|
let imageTextAttachment = NSTextAttachment()
|
||||||
|
let imageAttribute = NSAttributedString(attachment: imageTextAttachment)
|
||||||
|
imageTextAttachment.image = image
|
||||||
|
attributedString.append(imageAttribute)
|
||||||
|
return attributedString
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerIconLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let headerInfoLabel: ActiveLabel = {
|
||||||
|
let label = ActiveLabel(style: .statusHeader)
|
||||||
|
label.text = "Bob reblogged"
|
||||||
|
label.layer.masksToBounds = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let avatarView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.isAccessibilityElement = true
|
||||||
|
view.accessibilityTraits = .button
|
||||||
|
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
let avatarImageView: UIImageView = FLAnimatedImageView()
|
||||||
|
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||||
|
|
||||||
|
let nameLabel: ActiveLabel = {
|
||||||
|
let label = ActiveLabel(style: .statusName)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let nameTrialingDotLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = .systemFont(ofSize: 17)
|
||||||
|
label.text = "·"
|
||||||
|
label.isAccessibilityElement = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let usernameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.text = "@alice"
|
||||||
|
label.isAccessibilityElement = false
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let dateLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.text = "1d"
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let contentMetaText: MetaText = {
|
||||||
|
let metaText = MetaText()
|
||||||
|
metaText.textView.backgroundColor = .clear
|
||||||
|
metaText.textView.isEditable = false
|
||||||
|
metaText.textView.isSelectable = false
|
||||||
|
metaText.textView.isScrollEnabled = false
|
||||||
|
metaText.textView.textContainer.lineFragmentPadding = 0
|
||||||
|
metaText.textView.textContainerInset = .zero
|
||||||
|
metaText.textView.layer.masksToBounds = false
|
||||||
|
return metaText
|
||||||
|
}()
|
||||||
|
|
||||||
|
let statusContainerStackView = UIStackView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReplicaStatusView {
|
||||||
|
private func _init() {
|
||||||
|
// container: [reblog | author | status | action toolbar]
|
||||||
|
// note: do not set spacing for nested stackView to avoid SDK layout conflict issue
|
||||||
|
containerStackView.axis = .vertical
|
||||||
|
// containerStackView.spacing = 10
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(containerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||||
|
])
|
||||||
|
containerStackView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
|
containerStackView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
|
// header container: [icon | info]
|
||||||
|
let headerContainerStackView = UIStackView()
|
||||||
|
headerContainerStackView.axis = .horizontal
|
||||||
|
headerContainerStackView.spacing = 4
|
||||||
|
headerContainerStackView.addArrangedSubview(headerIconLabel)
|
||||||
|
headerContainerStackView.addArrangedSubview(headerInfoLabel)
|
||||||
|
headerIconLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
|
||||||
|
headerContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
headerContainerView.addSubview(headerContainerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
headerContainerStackView.topAnchor.constraint(equalTo: headerContainerView.topAnchor),
|
||||||
|
headerContainerStackView.leadingAnchor.constraint(equalTo: headerContainerView.leadingAnchor),
|
||||||
|
headerContainerStackView.trailingAnchor.constraint(equalTo: headerContainerView.trailingAnchor),
|
||||||
|
headerContainerView.bottomAnchor.constraint(equalTo: headerContainerStackView.bottomAnchor, constant: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
containerStackView.addArrangedSubview(headerContainerView)
|
||||||
|
defer {
|
||||||
|
containerStackView.bringSubviewToFront(headerContainerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// author container: [avatar | author meta container | reveal button]
|
||||||
|
let authorContainerStackView = UIStackView()
|
||||||
|
authorContainerStackView.axis = .horizontal
|
||||||
|
authorContainerStackView.spacing = StatusView.avatarToLabelSpacing
|
||||||
|
authorContainerStackView.distribution = .fill
|
||||||
|
|
||||||
|
// avatar
|
||||||
|
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
authorContainerStackView.addArrangedSubview(avatarView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.required - 1),
|
||||||
|
avatarView.heightAnchor.constraint(equalToConstant: StatusView.avatarImageSize.height).priority(.required - 1),
|
||||||
|
])
|
||||||
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarView.addSubview(avatarImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
||||||
|
avatarImageView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
||||||
|
avatarImageView.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
||||||
|
avatarImageView.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
||||||
|
])
|
||||||
|
avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarView.addSubview(avatarStackedContainerButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarStackedContainerButton.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
||||||
|
avatarStackedContainerButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
||||||
|
avatarStackedContainerButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor),
|
||||||
|
avatarStackedContainerButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
// author meta container: [title container | subtitle container]
|
||||||
|
let authorMetaContainerStackView = UIStackView()
|
||||||
|
authorContainerStackView.addArrangedSubview(authorMetaContainerStackView)
|
||||||
|
authorMetaContainerStackView.axis = .vertical
|
||||||
|
authorMetaContainerStackView.spacing = 4
|
||||||
|
|
||||||
|
// title container: [display name | "·" | date | padding]
|
||||||
|
let titleContainerStackView = UIStackView()
|
||||||
|
authorMetaContainerStackView.addArrangedSubview(titleContainerStackView)
|
||||||
|
titleContainerStackView.axis = .horizontal
|
||||||
|
titleContainerStackView.spacing = 4
|
||||||
|
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
titleContainerStackView.addArrangedSubview(nameLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
nameLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
titleContainerStackView.alignment = .firstBaseline
|
||||||
|
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
|
||||||
|
titleContainerStackView.addArrangedSubview(dateLabel)
|
||||||
|
let padding = UIView()
|
||||||
|
titleContainerStackView.addArrangedSubview(padding) // padding
|
||||||
|
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||||
|
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
||||||
|
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||||
|
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
|
padding.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
|
||||||
|
padding.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
|
||||||
|
|
||||||
|
// subtitle container: [username]
|
||||||
|
let subtitleContainerStackView = UIStackView()
|
||||||
|
authorMetaContainerStackView.addArrangedSubview(subtitleContainerStackView)
|
||||||
|
subtitleContainerStackView.axis = .horizontal
|
||||||
|
subtitleContainerStackView.addArrangedSubview(usernameLabel)
|
||||||
|
|
||||||
|
authorContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
authorContainerView.addSubview(authorContainerStackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
authorContainerStackView.topAnchor.constraint(equalTo: authorContainerView.topAnchor),
|
||||||
|
authorContainerStackView.leadingAnchor.constraint(equalTo: authorContainerView.leadingAnchor),
|
||||||
|
authorContainerStackView.trailingAnchor.constraint(equalTo: authorContainerView.trailingAnchor),
|
||||||
|
authorContainerView.bottomAnchor.constraint(equalTo: authorContainerStackView.bottomAnchor, constant: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
containerStackView.addArrangedSubview(authorContainerView)
|
||||||
|
|
||||||
|
// status container: [status]
|
||||||
|
containerStackView.addArrangedSubview(statusContainerStackView)
|
||||||
|
statusContainerStackView.axis = .vertical
|
||||||
|
statusContainerStackView.spacing = 10
|
||||||
|
|
||||||
|
// avoid overlay behind other views
|
||||||
|
defer {
|
||||||
|
containerStackView.bringSubviewToFront(authorContainerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// status
|
||||||
|
statusContainerStackView.addArrangedSubview(contentMetaText.textView)
|
||||||
|
contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
|
avatarStackedContainerButton.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AvatarConfigurableView
|
||||||
|
extension ReplicaStatusView: AvatarConfigurableView {
|
||||||
|
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
|
||||||
|
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
|
||||||
|
var configurableAvatarImageView: UIImageView? { avatarImageView }
|
||||||
|
var configurableAvatarButton: UIButton? { nil }
|
||||||
|
}
|
|
@ -8,19 +8,12 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class StatusContentWarningEditorView: UIView {
|
final class StatusContentWarningEditorView: UIView {
|
||||||
|
|
||||||
let containerView: UIView = {
|
// due to section following readable inset. We overlap the bleeding to make background fill
|
||||||
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
|
// default hidden
|
||||||
let containerBackgroundView: UIView = {
|
let containerBackgroundView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
view.isHidden = true
|
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -55,44 +48,38 @@ final class StatusContentWarningEditorView: UIView {
|
||||||
|
|
||||||
extension StatusContentWarningEditorView {
|
extension StatusContentWarningEditorView {
|
||||||
private func _init() {
|
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
|
containerBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
containerView.addSubview(containerBackgroundView)
|
addSubview(containerBackgroundView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
containerBackgroundView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
containerBackgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
containerBackgroundView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: -1024),
|
containerBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -1024),
|
||||||
containerBackgroundView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 1024),
|
containerBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 1024),
|
||||||
containerBackgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
containerBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false
|
iconImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
containerView.addSubview(iconImageView)
|
addSubview(iconImageView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
iconImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
iconImageView.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor),
|
iconImageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
iconImageView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.defaultHigh), // center alignment to avatar
|
iconImageView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.defaultHigh), // center alignment to avatar
|
||||||
])
|
])
|
||||||
iconImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
iconImageView.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||||
|
|
||||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
containerView.addSubview(textView)
|
addSubview(textView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
textView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 6),
|
textView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
textView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: StatusView.avatarToLabelSpacing - 4), // align to name label. minus magic 4pt to remove addtion inset
|
textView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 6).priority(.required - 1),
|
||||||
textView.trailingAnchor.constraint(equalTo: containerView.readableContentGuide.trailingAnchor),
|
textView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: StatusView.avatarToLabelSpacing - 4), // align to name label. minus magic 4pt to remove addition inset
|
||||||
containerView.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: 6),
|
textView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
bottomAnchor.constraint(greaterThanOrEqualTo: textView.bottomAnchor, constant: 6).priority(.required - 1),
|
||||||
|
//textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
textView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
|
textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
import MetaTextView
|
||||||
|
import Meta
|
||||||
|
|
||||||
final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
static let actionImageBorderWidth: CGFloat = 2
|
static let actionImageBorderWidth: CGFloat = 2
|
||||||
|
@ -255,6 +257,10 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
|
||||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
import Meta
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
|
final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
|
|
||||||
|
@ -203,4 +205,8 @@ extension ReportedStatusTableViewCell: StatusViewDelegate {
|
||||||
|
|
||||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,15 +98,15 @@ extension AudioContainerView {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor),
|
playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor),
|
||||||
playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor),
|
playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor),
|
||||||
playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32),
|
playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||||
playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32),
|
playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||||
])
|
])
|
||||||
|
|
||||||
container.addArrangedSubview(slider)
|
container.addArrangedSubview(slider)
|
||||||
|
|
||||||
container.addArrangedSubview(timeLabel)
|
container.addArrangedSubview(timeLabel)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
timeLabel.widthAnchor.constraint(equalToConstant: 40),
|
timeLabel.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import AVKit
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
import FLAnimatedImage
|
import FLAnimatedImage
|
||||||
|
import MetaTextView
|
||||||
|
import Meta
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// import LinkPresentation
|
// import LinkPresentation
|
||||||
|
@ -24,9 +26,12 @@ protocol StatusViewDelegate: AnyObject {
|
||||||
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||||
|
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class StatusView: UIView {
|
final class StatusView: UIView {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "StatusView", category: "logic")
|
||||||
|
|
||||||
var statusPollTableViewHeightObservation: NSKeyValueObservation?
|
var statusPollTableViewHeightObservation: NSKeyValueObservation?
|
||||||
var pollCountdownSubscription: AnyCancellable?
|
var pollCountdownSubscription: AnyCancellable?
|
||||||
|
@ -78,6 +83,7 @@ final class StatusView: UIView {
|
||||||
let headerInfoLabel: ActiveLabel = {
|
let headerInfoLabel: ActiveLabel = {
|
||||||
let label = ActiveLabel(style: .statusHeader)
|
let label = ActiveLabel(style: .statusHeader)
|
||||||
label.text = "Bob reblogged"
|
label.text = "Bob reblogged"
|
||||||
|
label.layer.masksToBounds = false
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -201,7 +207,17 @@ final class StatusView: UIView {
|
||||||
return actionToolbarContainer
|
return actionToolbarContainer
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let activeTextLabel = ActiveLabel(style: .default)
|
let contentMetaText: MetaText = {
|
||||||
|
let metaText = MetaText()
|
||||||
|
metaText.textView.backgroundColor = .clear
|
||||||
|
metaText.textView.isEditable = false
|
||||||
|
metaText.textView.isSelectable = false
|
||||||
|
metaText.textView.isScrollEnabled = false
|
||||||
|
metaText.textView.textContainer.lineFragmentPadding = 0
|
||||||
|
metaText.textView.textContainerInset = .zero
|
||||||
|
metaText.textView.layer.masksToBounds = false
|
||||||
|
return metaText
|
||||||
|
}()
|
||||||
|
|
||||||
private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
|
||||||
|
@ -261,6 +277,9 @@ extension StatusView {
|
||||||
headerContainerView.bottomAnchor.constraint(equalTo: headerContainerStackView.bottomAnchor, constant: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
headerContainerView.bottomAnchor.constraint(equalTo: headerContainerStackView.bottomAnchor, constant: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
containerStackView.addArrangedSubview(headerContainerView)
|
containerStackView.addArrangedSubview(headerContainerView)
|
||||||
|
defer {
|
||||||
|
containerStackView.bringSubviewToFront(headerContainerView)
|
||||||
|
}
|
||||||
|
|
||||||
// author container: [avatar | author meta container | reveal button]
|
// author container: [avatar | author meta container | reveal button]
|
||||||
let authorContainerStackView = UIStackView()
|
let authorContainerStackView = UIStackView()
|
||||||
|
@ -360,8 +379,8 @@ extension StatusView {
|
||||||
}
|
}
|
||||||
|
|
||||||
// status
|
// status
|
||||||
statusContainerStackView.addArrangedSubview(activeTextLabel)
|
statusContainerStackView.addArrangedSubview(contentMetaText.textView)
|
||||||
activeTextLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// link preview
|
// link preview
|
||||||
|
@ -423,8 +442,9 @@ extension StatusView {
|
||||||
|
|
||||||
avatarStackedContainerButton.isHidden = true
|
avatarStackedContainerButton.isHidden = true
|
||||||
contentWarningOverlayView.isHidden = true
|
contentWarningOverlayView.isHidden = true
|
||||||
|
|
||||||
activeTextLabel.delegate = self
|
contentMetaText.textView.delegate = self
|
||||||
|
contentMetaText.textView.linkDelegate = self
|
||||||
playerContainerView.delegate = self
|
playerContainerView.delegate = self
|
||||||
contentWarningOverlayView.delegate = self
|
contentWarningOverlayView.delegate = self
|
||||||
|
|
||||||
|
@ -515,6 +535,34 @@ extension StatusView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - MetaTextViewDelegate
|
||||||
|
extension StatusView: MetaTextViewDelegate {
|
||||||
|
func metaTextView(_ metaTextView: MetaTextView, didSelectLink link: URL) {
|
||||||
|
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||||
|
switch metaTextView {
|
||||||
|
case contentMetaText.textView:
|
||||||
|
guard let meta = Meta(url: link) else { return }
|
||||||
|
delegate?.statusView(self, metaText: contentMetaText, didSelectMeta: meta)
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension StatusView: UITextViewDelegate {
|
||||||
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
|
switch textView {
|
||||||
|
case contentMetaText.textView:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ActiveLabelDelegate
|
// MARK: - ActiveLabelDelegate
|
||||||
extension StatusView: ActiveLabelDelegate {
|
extension StatusView: ActiveLabelDelegate {
|
||||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
import Meta
|
||||||
|
import MetaTextView
|
||||||
|
|
||||||
protocol StatusTableViewCellDelegate: AnyObject {
|
protocol StatusTableViewCellDelegate: AnyObject {
|
||||||
var context: AppContext! { get }
|
var context: AppContext! { get }
|
||||||
|
@ -26,6 +28,7 @@ protocol StatusTableViewCellDelegate: AnyObject {
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||||
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||||
|
@ -71,6 +74,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
selectionStyle = .default
|
selectionStyle = .default
|
||||||
|
statusView.contentMetaText.textView.isSelectable = false
|
||||||
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
||||||
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true
|
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true
|
||||||
statusView.pollTableView.dataSource = nil
|
statusView.pollTableView.dataSource = nil
|
||||||
|
@ -301,6 +305,10 @@ extension StatusTableViewCell: StatusViewDelegate {
|
||||||
delegate?.statusTableViewCell(self, statusView: statusView, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
delegate?.statusTableViewCell(self, statusView: statusView, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||||
|
delegate?.statusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MosaicImageViewDelegate
|
// MARK: - MosaicImageViewDelegate
|
||||||
|
|
|
@ -33,6 +33,7 @@ extension EmojiService {
|
||||||
}()
|
}()
|
||||||
let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([])
|
let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([])
|
||||||
let emojiDict = CurrentValueSubject<[String: [Mastodon.Entity.Emoji]], Never>([:])
|
let emojiDict = CurrentValueSubject<[String: [Mastodon.Entity.Emoji]], Never>([:])
|
||||||
|
let emojiMapping = CurrentValueSubject<[String: String], Never>([:])
|
||||||
let emojiTrie = CurrentValueSubject<Trie<Character>?, Never>(nil)
|
let emojiTrie = CurrentValueSubject<Trie<Character>?, Never>(nil)
|
||||||
|
|
||||||
private var learnedEmoji: Set<String> = Set()
|
private var learnedEmoji: Set<String> = Set()
|
||||||
|
@ -45,6 +46,18 @@ extension EmojiService {
|
||||||
.map { Dictionary(grouping: $0, by: { $0.shortcode }) }
|
.map { Dictionary(grouping: $0, by: { $0.shortcode }) }
|
||||||
.assign(to: \.value, on: emojiDict)
|
.assign(to: \.value, on: emojiDict)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
emojiDict
|
||||||
|
.map { dict in
|
||||||
|
var mapping: [String: String] = [:]
|
||||||
|
for (key, values) in dict {
|
||||||
|
guard let emoji = values.first else { continue }
|
||||||
|
mapping[key] = emoji.url
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: emojiMapping)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
emojis
|
emojis
|
||||||
.map { emojis -> Trie<Character>? in
|
.map { emojis -> Trie<Character>? in
|
||||||
|
|
|
@ -59,8 +59,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppDelegate {
|
extension AppDelegate {
|
||||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||||
|
#if DEBUG
|
||||||
|
return .all
|
||||||
|
#else
|
||||||
return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all
|
return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue