feat: [WIP] add auto completion view for compose highlight
This commit is contained in:
parent
761d094832
commit
c2c38c9307
|
@ -206,8 +206,6 @@
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
||||||
DB35B0B32643D821006AC73B /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB35B0B22643D821006AC73B /* TwitterTextEditor */; };
|
|
||||||
DB35B0B42643D821006AC73B /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB35B0B22643D821006AC73B /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
|
||||||
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 */; };
|
||||||
|
@ -311,6 +309,11 @@
|
||||||
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
|
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
|
||||||
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
|
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
|
||||||
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
|
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
|
||||||
|
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; };
|
||||||
|
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; };
|
||||||
|
DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
|
DB6F5E35264E78E7009108F4 /* AutoCompletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompletionViewController.swift */; };
|
||||||
|
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; };
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
||||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
|
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
|
||||||
|
@ -527,7 +530,7 @@
|
||||||
files = (
|
files = (
|
||||||
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */,
|
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */,
|
||||||
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
|
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
|
||||||
DB35B0B42643D821006AC73B /* TwitterTextEditor in Embed Frameworks */,
|
DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */,
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -853,6 +856,9 @@
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
||||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
|
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
|
||||||
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
|
||||||
|
DB6F5E34264E78E7009108F4 /* AutoCompletionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompletionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = "<group>"; };
|
||||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||||
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -993,7 +999,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
DB35B0B32643D821006AC73B /* TwitterTextEditor in Frameworks */,
|
DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */,
|
||||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||||
|
@ -1874,6 +1880,15 @@
|
||||||
path = MastodonSDK;
|
path = MastodonSDK;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB6F5E36264E78EA009108F4 /* AutoCompletion */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB6F5E34264E78E7009108F4 /* AutoCompletionViewController.swift */,
|
||||||
|
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */,
|
||||||
|
);
|
||||||
|
path = AutoCompletion;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB72602125E36A2500235243 /* ServerRules */ = {
|
DB72602125E36A2500235243 /* ServerRules */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1894,6 +1909,7 @@
|
||||||
DB789A1025F9F29B0071ACA0 /* Compose */ = {
|
DB789A1025F9F29B0071ACA0 /* Compose */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB6F5E36264E78EA009108F4 /* AutoCompletion */,
|
||||||
DB55D32225FB4D320002F825 /* View */,
|
DB55D32225FB4D320002F825 /* View */,
|
||||||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||||
|
@ -2199,6 +2215,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
||||||
|
DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */,
|
||||||
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
||||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */,
|
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */,
|
||||||
);
|
);
|
||||||
|
@ -2400,7 +2417,7 @@
|
||||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||||
DBB525072611EAC0002F1F29 /* Tabman */,
|
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||||
DB35B0B22643D821006AC73B /* TwitterTextEditor */,
|
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -2589,7 +2606,7 @@
|
||||||
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
||||||
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */,
|
||||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||||
DB35B0B12643D821006AC73B /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -3035,6 +3052,7 @@
|
||||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
||||||
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
|
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
|
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
|
||||||
|
@ -3068,6 +3086,7 @@
|
||||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
|
DB6F5E35264E78E7009108F4 /* AutoCompletionViewController.swift in Sources */,
|
||||||
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||||
|
@ -3160,6 +3179,7 @@
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
||||||
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
||||||
|
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */,
|
||||||
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||||
|
@ -3938,14 +3958,6 @@
|
||||||
minimumVersion = 0.1.1;
|
minimumVersion = 0.1.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
DB35B0B12643D821006AC73B /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/twitter/TwitterTextEditor";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 1.1.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = {
|
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/Alamofire/AlamofireImage.git";
|
repositoryURL = "https://github.com/Alamofire/AlamofireImage.git";
|
||||||
|
@ -3970,6 +3982,14 @@
|
||||||
minimumVersion = 4.2.2;
|
minimumVersion = 4.2.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git";
|
||||||
|
requirement = {
|
||||||
|
branch = "feature/expose-layout";
|
||||||
|
kind = branch;
|
||||||
|
};
|
||||||
|
};
|
||||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
||||||
|
@ -4031,11 +4051,6 @@
|
||||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||||
productName = CommonOSLog;
|
productName = CommonOSLog;
|
||||||
};
|
};
|
||||||
DB35B0B22643D821006AC73B /* TwitterTextEditor */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = DB35B0B12643D821006AC73B /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
|
||||||
productName = TwitterTextEditor;
|
|
||||||
};
|
|
||||||
DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
|
DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||||
|
@ -4056,6 +4071,11 @@
|
||||||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||||
productName = AlamofireImage;
|
productName = AlamofireImage;
|
||||||
};
|
};
|
||||||
|
DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||||
|
productName = TwitterTextEditor;
|
||||||
|
};
|
||||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||||
|
|
|
@ -138,11 +138,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "TwitterTextEditor",
|
"package": "TwitterTextEditor",
|
||||||
"repositoryURL": "https://github.com/twitter/TwitterTextEditor",
|
"repositoryURL": "https://github.com/MainasuK/TwitterTextEditor.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": "feature/expose-layout",
|
||||||
"revision": "dfe0edc3bcb6703ee2fd0e627f95e726b63e732a",
|
"revision": "c208329b23dcb3c8c7192de34776440d625a26a4",
|
||||||
"version": "1.1.0"
|
"version": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -37,6 +37,7 @@ extension ComposeStatusSection {
|
||||||
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
|
textEditorViewChangeObserver: TextEditorViewChangeObserver,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
|
@ -45,6 +46,7 @@ extension ComposeStatusSection {
|
||||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
UICollectionViewDiffableDataSource(collectionView: collectionView) { [
|
||||||
weak customEmojiPickerInputViewModel,
|
weak customEmojiPickerInputViewModel,
|
||||||
weak textEditorViewTextAttributesDelegate,
|
weak textEditorViewTextAttributesDelegate,
|
||||||
|
weak textEditorViewChangeObserver,
|
||||||
weak composeStatusAttachmentTableViewCellDelegate,
|
weak composeStatusAttachmentTableViewCellDelegate,
|
||||||
weak composeStatusPollOptionCollectionViewCellDelegate,
|
weak composeStatusPollOptionCollectionViewCellDelegate,
|
||||||
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
weak composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||||
|
@ -92,6 +94,7 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute)
|
||||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||||
|
cell.textEditorViewChangeObserver = textEditorViewChangeObserver // relay
|
||||||
cell.composeContent
|
cell.composeContent
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
|
|
@ -152,41 +152,4 @@ extension ActiveLabel {
|
||||||
return elements
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
// public override func accessibilityElementCount() -> Int {
|
|
||||||
// return 1 + activeEntities.count
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public override func accessibilityElement(at index: Int) -> Any? {
|
|
||||||
// if index == 0 {
|
|
||||||
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
|
||||||
// element.accessibilityTraits = .staticText
|
|
||||||
// element.accessibilityLabel = accessibilityLabel
|
|
||||||
// element.accessibilityFrame = superview!.convert(frame, to: nil)
|
|
||||||
// element.index = index
|
|
||||||
// return element
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let index = index - 1
|
|
||||||
// guard index < activeEntities.count else { return nil }
|
|
||||||
// let eneity = activeEntities[index]
|
|
||||||
// guard let element = eneity.accessibilityElement(in: self) else { return nil }
|
|
||||||
//
|
|
||||||
// var glyphRange = NSRange()
|
|
||||||
// layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
|
||||||
// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
|
||||||
// element.accessibilityFrame = self.convert(rect, to: nil)
|
|
||||||
// element.accessibilityContainer = self
|
|
||||||
//
|
|
||||||
// return element
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public override func index(ofAccessibilityElement element: Any) -> Int {
|
|
||||||
// guard let element = element as? ActiveLabelAccessibilityElement,
|
|
||||||
// let index = element.index else {
|
|
||||||
// return NSNotFound
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return index
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// MastodonRegex.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum MastodonRegex {
|
||||||
|
/// mention, hashtag.
|
||||||
|
/// @...
|
||||||
|
/// #...
|
||||||
|
static let highlightPattern = "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))"
|
||||||
|
/// emoji
|
||||||
|
/// :shortcode:
|
||||||
|
/// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
||||||
|
/// precondition :\B with following space
|
||||||
|
static let emojiPattern = "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))"
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// AutoCompleteTopChevronView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AutoCompleteTopChevronView: UIView {
|
||||||
|
|
||||||
|
static let chevronSize = CGSize(width: 20, height: 12)
|
||||||
|
|
||||||
|
var chevronMinX: CGFloat = 0
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func _init() {
|
||||||
|
backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
let bezierPath = UIBezierPath()
|
||||||
|
let bottomY = rect.height
|
||||||
|
let topY = 0
|
||||||
|
let count = Int(ceil(rect.width / CGFloat(SawToothView.widthUint)))
|
||||||
|
bezierPath.move(to: CGPoint(x: 0, y: bottomY))
|
||||||
|
for n in 0 ..< count {
|
||||||
|
bezierPath.addLine(to: CGPoint(x: CGFloat((Double(n) + 0.5) * Double(SawToothView.widthUint)), y: CGFloat(topY)))
|
||||||
|
bezierPath.addLine(to: CGPoint(x: CGFloat((Double(n) + 1) * Double(SawToothView.widthUint)), y: CGFloat(bottomY)))
|
||||||
|
}
|
||||||
|
bezierPath.addLine(to: CGPoint(x: 0, y: bottomY))
|
||||||
|
bezierPath.close()
|
||||||
|
Asset.Colors.Background.systemBackground.color.setFill()
|
||||||
|
bezierPath.fill()
|
||||||
|
bezierPath.lineWidth = 0
|
||||||
|
bezierPath.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// AutoCompletionViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-5-14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AutoCompletionViewController: UIViewController {
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutoCompletionViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .red.withAlphaComponent(0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
let statusContentWarningEditorView = StatusContentWarningEditorView()
|
||||||
|
|
||||||
|
let textEditorViewContainerView = UIView()
|
||||||
let textEditorView: TextEditorView = {
|
let textEditorView: TextEditorView = {
|
||||||
let textEditorView = TextEditorView()
|
let textEditorView = TextEditorView()
|
||||||
textEditorView.font = .preferredFont(forTextStyle: .body)
|
textEditorView.font = .preferredFont(forTextStyle: .body)
|
||||||
|
@ -27,6 +28,9 @@ final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
||||||
textEditorView.keyboardType = .twitter
|
textEditorView.keyboardType = .twitter
|
||||||
return textEditorView
|
return textEditorView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// input
|
||||||
|
weak var textEditorViewChangeObserver: TextEditorViewChangeObserver?
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let composeContent = PassthroughSubject<String, Never>()
|
let composeContent = PassthroughSubject<String, Never>()
|
||||||
|
@ -75,13 +79,23 @@ extension ComposeStatusContentCollectionViewCell {
|
||||||
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
textEditorViewContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(textEditorView)
|
contentView.addSubview(textEditorViewContainerView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
textEditorViewContainerView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||||
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
textEditorViewContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
textEditorViewContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 10),
|
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.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||||
|
@ -98,6 +112,10 @@ extension ComposeStatusContentCollectionViewCell {
|
||||||
// MARK: - TextEditorViewChangeObserver
|
// MARK: - TextEditorViewChangeObserver
|
||||||
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
||||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
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)
|
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 }
|
guard changeResult.isTextChanged else { return }
|
||||||
composeContent.send(textEditorView.text)
|
composeContent.send(textEditorView.text)
|
||||||
|
|
|
@ -15,6 +15,8 @@ import TwitterTextEditor
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
static let minAutoCompletionVisibleHeight: CGFloat = 100
|
||||||
|
|
||||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
@ -93,6 +95,12 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
return documentPickerController
|
return documentPickerController
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var autoCompletionViewController: AutoCompletionViewController = {
|
||||||
|
let viewController = AutoCompletionViewController()
|
||||||
|
viewController.context = context
|
||||||
|
return viewController
|
||||||
|
}()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
|
@ -166,6 +174,7 @@ extension ComposeViewController {
|
||||||
dependency: self,
|
dependency: self,
|
||||||
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: self,
|
textEditorViewTextAttributesDelegate: self,
|
||||||
|
textEditorViewChangeObserver: self,
|
||||||
composeStatusAttachmentTableViewCellDelegate: self,
|
composeStatusAttachmentTableViewCellDelegate: self,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: self,
|
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
||||||
|
@ -181,26 +190,27 @@ extension ComposeViewController {
|
||||||
dependency: self
|
dependency: self
|
||||||
)
|
)
|
||||||
|
|
||||||
// respond scrollView overlap change
|
|
||||||
//view.layoutIfNeeded()
|
|
||||||
// update layout when keyboard show/dismiss
|
// update layout when keyboard show/dismiss
|
||||||
Publishers.CombineLatest4(
|
let keyboardEventPublishers = Publishers.CombineLatest3(
|
||||||
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.isShow,
|
||||||
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.state,
|
||||||
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.endFrame
|
||||||
viewModel.isCustomEmojiComposing.eraseToAnyPublisher()
|
|
||||||
)
|
)
|
||||||
.sink(receiveValue: { [weak self] isShow, state, endFrame, isCustomEmojiComposing in
|
Publishers.CombineLatest3(
|
||||||
|
keyboardEventPublishers,
|
||||||
|
viewModel.isCustomEmojiComposing,
|
||||||
|
viewModel.autoCompletion
|
||||||
|
)
|
||||||
|
.sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, autoCompletion in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let (isShow, state, endFrame) = keyboardEvents
|
||||||
let extraMargin: CGFloat = {
|
let extraMargin: CGFloat = {
|
||||||
if self.view.safeAreaInsets.bottom == .zero {
|
var margin = self.composeToolbarView.frame.height
|
||||||
// needs extra margin for zero inset device to workaround UIKit issue
|
if autoCompletion != nil {
|
||||||
return self.composeToolbarView.frame.height
|
margin += ComposeViewController.minAutoCompletionVisibleHeight
|
||||||
} else {
|
|
||||||
// default some magic 16 extra margin
|
|
||||||
return 16
|
|
||||||
}
|
}
|
||||||
|
return margin
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// update keyboard background color
|
// update keyboard background color
|
||||||
|
@ -221,27 +231,44 @@ extension ComposeViewController {
|
||||||
self.systemKeyboardHeight = endFrame.height
|
self.systemKeyboardHeight = endFrame.height
|
||||||
|
|
||||||
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
||||||
let padding = contentFrame.maxY - 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.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
|
||||||
UIView.animate(withDuration: 0.3) {
|
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
self.updateKeyboardBackground(isKeyboardDisplay: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.collectionView.contentInset.bottom = padding + extraMargin
|
self.collectionView.contentInset.bottom = padding
|
||||||
self.collectionView.verticalScrollIndicatorInsets.bottom = padding + extraMargin
|
self.collectionView.verticalScrollIndicatorInsets.bottom = padding
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = padding
|
self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
self.updateKeyboardBackground(isKeyboardDisplay: isShow)
|
self.updateKeyboardBackground(isKeyboardDisplay: isShow)
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind auto-complete
|
||||||
|
viewModel.autoCompletion
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] autoCompletion in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let textEditorView = self.textEditorView() else { return }
|
||||||
|
if self.autoCompletionViewController.view.superview == nil {
|
||||||
|
self.autoCompletionViewController.view.frame = self.view.bounds
|
||||||
|
self.autoCompletionViewController.willMove(toParent: self)
|
||||||
|
// add to container view. seealso: `viewDidLayoutSubviews()`
|
||||||
|
textEditorView.superview!.addSubview(self.autoCompletionViewController.view)
|
||||||
|
self.autoCompletionViewController.didMove(toParent: self)
|
||||||
|
self.autoCompletionViewController.view.isHidden = true
|
||||||
|
}
|
||||||
|
self.autoCompletionViewController.view.isHidden = autoCompletion == nil
|
||||||
|
guard let autoCompletion = autoCompletion else { return }
|
||||||
|
self.autoCompletionViewController.view.frame.origin.y = autoCompletion.textBoundingRect.maxY
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind publish bar button state
|
// bind publish bar button state
|
||||||
viewModel.isPublishBarButtonItemEnabled
|
viewModel.isPublishBarButtonItemEnabled
|
||||||
|
@ -382,6 +409,18 @@ extension ComposeViewController {
|
||||||
viewModel.traitCollectionDidChangePublisher.send()
|
viewModel.traitCollectionDidChangePublisher.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
// pin autoCompletionViewController frame to window
|
||||||
|
if let containerView = autoCompletionViewController.view.superview {
|
||||||
|
let viewFrameInWindow = containerView.convert(autoCompletionViewController.view.frame, to: nil)
|
||||||
|
if viewFrameInWindow.origin.x != 0 {
|
||||||
|
autoCompletionViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
@ -600,10 +639,8 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||||
|
|
||||||
let stringRange = NSRange(location: 0, length: string.length)
|
let stringRange = NSRange(location: 0, length: string.length)
|
||||||
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))")
|
let highlightMatches = string.matches(pattern: MastodonRegex.highlightPattern)
|
||||||
// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
let emojiMatches = string.matches(pattern: MastodonRegex.emojiPattern)
|
||||||
// precondition :\B with following space
|
|
||||||
let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
|
|
||||||
// only accept http/https scheme
|
// only accept http/https scheme
|
||||||
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||||
|
|
||||||
|
@ -729,6 +766,97 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - TextEditorViewChangeObserver
|
||||||
|
extension ComposeViewController: TextEditorViewChangeObserver {
|
||||||
|
|
||||||
|
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||||
|
guard var autoCompeletion = ComposeViewController.scanAutoCompletion(textEditorView: textEditorView) else {
|
||||||
|
viewModel.autoCompletion.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(autoCompeletion.toHighlightEndString), String(autoCompeletion.toCursorString))
|
||||||
|
|
||||||
|
var glyphRange = NSRange()
|
||||||
|
textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange)
|
||||||
|
let textContainer = textEditorView.layoutManager.textContainers[0]
|
||||||
|
let textBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||||
|
let textBoundingRectInWindow = textEditorView.convert(textBoundingRect, to: nil)
|
||||||
|
|
||||||
|
let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes.value
|
||||||
|
guard textBoundingRectInWindow.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
|
||||||
|
|
||||||
|
// set bounding rect and trigger layout
|
||||||
|
autoCompeletion.textBoundingRect = textBoundingRect
|
||||||
|
viewModel.autoCompletion.value = autoCompeletion
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AutoCompletion {
|
||||||
|
let toCursorRange: Range<String.Index>
|
||||||
|
let toCursorString: Substring
|
||||||
|
let toHighlightEndRange: Range<String.Index>
|
||||||
|
let toHighlightEndString: Substring
|
||||||
|
|
||||||
|
var textBoundingRect: CGRect = .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func scanAutoCompletion(textEditorView: TextEditorView) -> AutoCompletion? {
|
||||||
|
let text = textEditorView.text
|
||||||
|
let cursorLocation = textEditorView.selectedRange.location
|
||||||
|
let cursorIndex = text.index(text.startIndex, offsetBy: cursorLocation)
|
||||||
|
guard cursorLocation > 0, !text.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let _highlighStartIndex: String.Index? = {
|
||||||
|
var index = text.index(text.startIndex, offsetBy: cursorLocation - 1)
|
||||||
|
while index > text.startIndex {
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index = text.index(before: index)
|
||||||
|
}
|
||||||
|
assert(index == text.startIndex)
|
||||||
|
let char = text[index]
|
||||||
|
if char == "@" || char == "#" {
|
||||||
|
return index
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard let highlighStartIndex = _highlighStartIndex else { return nil }
|
||||||
|
let scanRange = NSRange(highlighStartIndex..<text.endIndex, in: text)
|
||||||
|
|
||||||
|
guard let match = text.firstMatch(pattern: MastodonRegex.highlightPattern, options: [], range: scanRange) else { return nil }
|
||||||
|
let matchRange = match.range(at: 0)
|
||||||
|
let matchStartIndex = text.index(text.startIndex, offsetBy: matchRange.location)
|
||||||
|
let matchEndIndex = text.index(matchStartIndex, offsetBy: matchRange.length)
|
||||||
|
|
||||||
|
guard matchStartIndex == highlighStartIndex, matchEndIndex >= cursorIndex else { return nil }
|
||||||
|
let toCursorRange = highlighStartIndex..<cursorIndex
|
||||||
|
let toCursorString = text[toCursorRange]
|
||||||
|
let toHighlightEndRange = matchStartIndex..<matchEndIndex
|
||||||
|
let toHighlightEndString = text[toHighlightEndRange]
|
||||||
|
let autoCompletion = AutoCompletion(
|
||||||
|
toCursorRange: toCursorRange,
|
||||||
|
toCursorString: toCursorString,
|
||||||
|
toHighlightEndRange: toHighlightEndRange,
|
||||||
|
toHighlightEndString: toHighlightEndString
|
||||||
|
)
|
||||||
|
return autoCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ComposeToolbarViewDelegate
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ extension ComposeViewModel {
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
|
textEditorViewChangeObserver: TextEditorViewChangeObserver,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
|
@ -30,6 +31,7 @@ extension ComposeViewModel {
|
||||||
repliedToCellFrameSubscriber: repliedToCellFrame,
|
repliedToCellFrameSubscriber: repliedToCellFrame,
|
||||||
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||||
|
textEditorViewChangeObserver: textEditorViewChangeObserver,
|
||||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||||
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
||||||
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||||
|
|
|
@ -31,6 +31,8 @@ final class ComposeViewModel {
|
||||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
||||||
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
|
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
|
||||||
|
let autoCompletion = CurrentValueSubject<ComposeViewController.AutoCompletion?, Never>(nil)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
|
|
Loading…
Reference in New Issue