forked from zelo72/mastodon-ios
Merge branch 'develop' into feature/welcome-illustration
# Conflicts: # Mastodon.xcodeproj/project.pbxproj
This commit is contained in:
commit
0b1b3ecbbd
|
@ -14,6 +14,10 @@
|
||||||
"vote_failure": {
|
"vote_failure": {
|
||||||
"title": "Vote Failure",
|
"title": "Vote Failure",
|
||||||
"poll_expired": "The poll has expired"
|
"poll_expired": "The poll has expired"
|
||||||
|
},
|
||||||
|
"discard_post_content": {
|
||||||
|
"title": "Discard Publish",
|
||||||
|
"message": "Confirm discard composed post content."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
|
@ -27,6 +31,7 @@
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
"discard": "Discard",
|
||||||
"take_photo": "Take photo",
|
"take_photo": "Take photo",
|
||||||
"save_photo": "Save photo",
|
"save_photo": "Save photo",
|
||||||
"sign_in": "Sign In",
|
"sign_in": "Sign In",
|
||||||
|
@ -71,17 +76,17 @@
|
||||||
},
|
},
|
||||||
"server_picker": {
|
"server_picker": {
|
||||||
"title": "Pick a Server,\nany server.",
|
"title": "Pick a Server,\nany server.",
|
||||||
"Button": {
|
"button": {
|
||||||
"Category": {
|
"category": {
|
||||||
"All": "All"
|
"All": "All"
|
||||||
},
|
},
|
||||||
"SeeLess": "See Less",
|
"see_less": "See Less",
|
||||||
"SeeMore": "See More"
|
"see_more": "See More"
|
||||||
},
|
},
|
||||||
"Label": {
|
"label": {
|
||||||
"Language": "LANGUAGE",
|
"language": "LANGUAGE",
|
||||||
"Users": "USERS",
|
"users": "USERS",
|
||||||
"Category": "CATEGORY"
|
"category": "CATEGORY"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": "Find a server or join your own..."
|
"placeholder": "Find a server or join your own..."
|
||||||
|
@ -179,6 +184,14 @@
|
||||||
},
|
},
|
||||||
"public_timeline": {
|
"public_timeline": {
|
||||||
"title": "Public"
|
"title": "Public"
|
||||||
|
},
|
||||||
|
"compose": {
|
||||||
|
"title": {
|
||||||
|
"new_post": "New Post",
|
||||||
|
"new_reply": "New Reply"
|
||||||
|
},
|
||||||
|
"content_input_placeholder": "Type or paste what's on your mind",
|
||||||
|
"compose_action": "Publish"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,14 +148,24 @@
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
|
||||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
|
||||||
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
|
||||||
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
||||||
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
||||||
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; };
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
|
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
||||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||||
|
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */; };
|
||||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
||||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
||||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; };
|
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; };
|
||||||
|
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; };
|
||||||
|
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
|
||||||
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
|
||||||
|
DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; };
|
||||||
|
DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
||||||
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
||||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||||
|
@ -164,6 +174,10 @@
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
||||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
||||||
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
||||||
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
||||||
|
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */; };
|
||||||
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */; };
|
||||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||||
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
||||||
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
@ -201,6 +215,8 @@
|
||||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; };
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; };
|
||||||
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
||||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
||||||
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
||||||
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
|
||||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
|
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
|
||||||
|
@ -255,6 +271,7 @@
|
||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
|
DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */,
|
||||||
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
|
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
|
@ -412,13 +429,21 @@
|
||||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
|
||||||
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
|
||||||
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = "<group>"; };
|
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = "<group>"; };
|
||||||
|
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
||||||
|
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
||||||
|
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
||||||
|
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = "<group>"; };
|
||||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
||||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
||||||
|
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TwitterTextEditor+String.swift"; sourceTree = "<group>"; };
|
||||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
||||||
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
||||||
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = "<group>"; };
|
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = "<group>"; };
|
||||||
|
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
|
||||||
|
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
|
||||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
||||||
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
||||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||||
|
@ -427,6 +452,10 @@
|
||||||
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>"; };
|
||||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
||||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.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>"; };
|
||||||
|
DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTootContentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
||||||
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -466,6 +495,8 @@
|
||||||
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
|
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
|
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
||||||
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||||
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
|
||||||
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
|
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
|
||||||
|
@ -483,6 +514,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
DB6672A325F9FDE500D60309 /* 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 */,
|
||||||
|
@ -658,6 +690,7 @@
|
||||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
|
||||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */,
|
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */,
|
||||||
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */,
|
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */,
|
||||||
|
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */,
|
||||||
);
|
);
|
||||||
path = Button;
|
path = Button;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -676,6 +709,7 @@
|
||||||
children = (
|
children = (
|
||||||
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
||||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
|
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
|
||||||
|
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */,
|
||||||
);
|
);
|
||||||
path = Vender;
|
path = Vender;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -689,6 +723,7 @@
|
||||||
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
||||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||||
|
DB49A61925FF327D00B98345 /* EmojiService */,
|
||||||
);
|
);
|
||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -745,6 +780,7 @@
|
||||||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
||||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||||
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -792,6 +828,7 @@
|
||||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
||||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||||
|
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
|
||||||
);
|
);
|
||||||
path = Item;
|
path = Item;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -968,6 +1005,7 @@
|
||||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||||
|
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||||
);
|
);
|
||||||
path = APIService;
|
path = APIService;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -982,6 +1020,16 @@
|
||||||
path = CoreData;
|
path = CoreData;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB49A61925FF327D00B98345 /* EmojiService */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB49A61325FF2C5600B98345 /* EmojiService.swift */,
|
||||||
|
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */,
|
||||||
|
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */,
|
||||||
|
);
|
||||||
|
path = EmojiService;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB5086CB25CC0DB400C2C187 /* Preference */ = {
|
DB5086CB25CC0DB400C2C187 /* Preference */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -990,6 +1038,14 @@
|
||||||
path = Preference;
|
path = Preference;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB55D32225FB4D320002F825 /* View */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
||||||
|
);
|
||||||
|
path = View;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB68A03825E900CC00CFDF14 /* Share */ = {
|
DB68A03825E900CC00CFDF14 /* Share */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1025,6 +1081,27 @@
|
||||||
path = ServerRules;
|
path = ServerRules;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB789A1025F9F29B0071ACA0 /* Compose */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB55D32225FB4D320002F825 /* View */,
|
||||||
|
DB789A2125F9F76D0071ACA0 /* TableViewCell */,
|
||||||
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||||
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
||||||
|
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
||||||
|
);
|
||||||
|
path = Compose;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB789A2125F9F76D0071ACA0 /* TableViewCell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */,
|
||||||
|
DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = TableViewCell;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1126,6 +1203,7 @@
|
||||||
DB9D6BEE25E4F5370051B173 /* Search */,
|
DB9D6BEE25E4F5370051B173 /* Search */,
|
||||||
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
DB9D6BFD25E4F57B0051B173 /* Notification */,
|
||||||
DB9D6C0825E4F5A60051B173 /* Profile */,
|
DB9D6C0825E4F5A60051B173 /* Profile */,
|
||||||
|
DB789A1025F9F29B0071ACA0 /* Compose */,
|
||||||
);
|
);
|
||||||
path = Scene;
|
path = Scene;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1309,6 +1387,7 @@
|
||||||
DB5086B725CC0D6400C2C187 /* Kingfisher */,
|
DB5086B725CC0D6400C2C187 /* Kingfisher */,
|
||||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
||||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||||
|
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -1438,6 +1517,7 @@
|
||||||
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
||||||
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
||||||
|
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -1628,10 +1708,12 @@
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
||||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||||
|
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
|
||||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||||
|
@ -1648,11 +1730,13 @@
|
||||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
||||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
||||||
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||||
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
|
||||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
|
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
|
||||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||||
|
@ -1661,6 +1745,7 @@
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
||||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||||
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
|
||||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||||
|
@ -1671,6 +1756,7 @@
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
||||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
||||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
|
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
|
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */,
|
||||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
|
@ -1678,6 +1764,7 @@
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||||
|
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||||
|
@ -1685,6 +1772,7 @@
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
||||||
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||||
|
@ -1705,6 +1793,7 @@
|
||||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
|
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.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 */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
|
@ -1745,6 +1834,7 @@
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||||
|
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
||||||
|
@ -1769,13 +1859,17 @@
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||||
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||||
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -2362,6 +2456,14 @@
|
||||||
minimumVersion = 6.1.0;
|
minimumVersion = 6.1.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/twitter/TwitterTextEditor.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -2404,6 +2506,11 @@
|
||||||
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
productName = Kingfisher;
|
productName = Kingfisher;
|
||||||
};
|
};
|
||||||
|
DB6672A225F9FDE500D60309 /* TwitterTextEditor */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||||
|
productName = TwitterTextEditor;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|
|
@ -99,6 +99,15 @@
|
||||||
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
|
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
|
||||||
"version": "2.6.0"
|
"version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "TwitterTextEditor",
|
||||||
|
"repositoryURL": "https://github.com/twitter/TwitterTextEditor.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,6 +47,9 @@ extension SceneCoordinator {
|
||||||
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
|
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
|
||||||
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
||||||
|
|
||||||
|
// compose
|
||||||
|
case compose(viewModel: ComposeViewModel)
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
case alertController(alertController: UIAlertController)
|
case alertController(alertController: UIAlertController)
|
||||||
|
|
||||||
|
@ -190,6 +193,10 @@ private extension SceneCoordinator {
|
||||||
let _viewController = MastodonResendEmailViewController()
|
let _viewController = MastodonResendEmailViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .compose(let viewModel):
|
||||||
|
let _viewController = ComposeViewController()
|
||||||
|
_viewController.viewModel = viewModel
|
||||||
|
viewController = _viewController
|
||||||
case .alertController(let alertController):
|
case .alertController(let alertController):
|
||||||
if let popoverPresentationController = alertController.popoverPresentationController {
|
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||||
assert(
|
assert(
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
enum ComposeStatusItem {
|
||||||
|
case replyTo(statusObjectID: NSManagedObjectID)
|
||||||
|
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusItem: Hashable { }
|
||||||
|
|
||||||
|
extension ComposeStatusItem {
|
||||||
|
final class ComposeStatusAttribute: Equatable, Hashable {
|
||||||
|
private let id = UUID()
|
||||||
|
|
||||||
|
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
|
||||||
|
let displayName = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let username = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let composeContent = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
|
||||||
|
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
||||||
|
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
||||||
|
lhs.displayName.value == rhs.displayName.value &&
|
||||||
|
lhs.username.value == rhs.username.value &&
|
||||||
|
lhs.composeContent.value == rhs.composeContent.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import TwitterTextEditor
|
||||||
|
|
||||||
|
enum ComposeStatusSection: Equatable, Hashable {
|
||||||
|
case repliedTo
|
||||||
|
case status
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusSection {
|
||||||
|
enum ComposeKind {
|
||||||
|
case post
|
||||||
|
case reply(repliedToStatusObjectID: NSManagedObjectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusSection {
|
||||||
|
static func tableViewDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
dependency: NeedsDependency,
|
||||||
|
managedObjectContext: NSManagedObjectContext,
|
||||||
|
composeKind: ComposeKind,
|
||||||
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
|
||||||
|
) -> UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
||||||
|
UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>(tableView: tableView) { [weak textEditorViewTextAttributesDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||||
|
switch item {
|
||||||
|
case .replyTo(let repliedToStatusObjectID):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell
|
||||||
|
// TODO:
|
||||||
|
return cell
|
||||||
|
case .input(let replyToTootObjectID, let attribute):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeTootContentTableViewCell.self), for: indexPath) as! ComposeTootContentTableViewCell
|
||||||
|
managedObjectContext.perform {
|
||||||
|
guard let replyToTootObjectID = replyToTootObjectID,
|
||||||
|
let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
|
||||||
|
cell.statusView.headerContainerStackView.isHidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cell.statusView.headerContainerStackView.isHidden = false
|
||||||
|
cell.statusView.headerInfoLabel.text = "[TODO] \(replyTo.author.displayName)"
|
||||||
|
}
|
||||||
|
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
||||||
|
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||||
|
// self size input cell
|
||||||
|
cell.composeContent
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { text in
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusSection {
|
||||||
|
static func configure(
|
||||||
|
cell: ComposeTootContentTableViewCell,
|
||||||
|
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||||
|
) {
|
||||||
|
// set avatar
|
||||||
|
attribute.avatarURL
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { avatarURL in
|
||||||
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
// set display name and username
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
attribute.displayName.eraseToAnyPublisher(),
|
||||||
|
attribute.username.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { displayName, username in
|
||||||
|
cell.statusView.nameLabel.text = displayName
|
||||||
|
cell.statusView.usernameLabel.text = username
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
|
// bind compose content
|
||||||
|
cell.composeContent
|
||||||
|
.map { $0 as String? }
|
||||||
|
.assign(to: \.value, on: attribute.composeContent)
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
|
@ -376,7 +376,7 @@ extension StatusSection {
|
||||||
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
|
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assertionFailure()
|
// assertionFailure()
|
||||||
cell.pollCountdownSubscription = nil
|
cell.pollCountdownSubscription = nil
|
||||||
cell.statusView.pollCountdownLabel.text = "-"
|
cell.statusView.pollCountdownLabel.text = "-"
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ internal enum Asset {
|
||||||
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
||||||
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
|
||||||
internal static let highlight = ColorAsset(name: "Colors/Button/highlight")
|
internal static let highlight = ColorAsset(name: "Colors/Button/highlight")
|
||||||
|
internal static let normal = ColorAsset(name: "Colors/Button/normal")
|
||||||
}
|
}
|
||||||
internal enum Icon {
|
internal enum Icon {
|
||||||
internal static let photo = ColorAsset(name: "Colors/Icon/photo")
|
internal static let photo = ColorAsset(name: "Colors/Icon/photo")
|
||||||
|
|
|
@ -19,6 +19,12 @@ internal enum L10n {
|
||||||
/// Please try again later.
|
/// Please try again later.
|
||||||
internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater")
|
internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater")
|
||||||
}
|
}
|
||||||
|
internal enum DiscardPostContent {
|
||||||
|
/// Confirm discard composed post content.
|
||||||
|
internal static let message = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Message")
|
||||||
|
/// Discard Publish
|
||||||
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title")
|
||||||
|
}
|
||||||
internal enum ServerError {
|
internal enum ServerError {
|
||||||
/// Server Error
|
/// Server Error
|
||||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
||||||
|
@ -46,6 +52,8 @@ internal enum L10n {
|
||||||
internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm")
|
internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm")
|
||||||
/// Continue
|
/// Continue
|
||||||
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
|
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
|
||||||
|
/// Discard
|
||||||
|
internal static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard")
|
||||||
/// Edit
|
/// Edit
|
||||||
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
|
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
|
||||||
/// OK
|
/// OK
|
||||||
|
@ -127,6 +135,18 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum Scene {
|
internal enum Scene {
|
||||||
|
internal enum Compose {
|
||||||
|
/// Publish
|
||||||
|
internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
|
||||||
|
/// Type or paste what's on your mind
|
||||||
|
internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
|
||||||
|
internal enum Title {
|
||||||
|
/// New Post
|
||||||
|
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")
|
||||||
|
/// New Reply
|
||||||
|
internal static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply")
|
||||||
|
}
|
||||||
|
}
|
||||||
internal enum ConfirmEmail {
|
internal enum ConfirmEmail {
|
||||||
/// We just sent an email to %@,\ntap the link to confirm your account.
|
/// We just sent an email to %@,\ntap the link to confirm your account.
|
||||||
internal static func subtitle(_ p1: Any) -> String {
|
internal static func subtitle(_ p1: Any) -> String {
|
||||||
|
@ -280,9 +300,9 @@ internal enum L10n {
|
||||||
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
|
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
|
||||||
internal enum Button {
|
internal enum Button {
|
||||||
/// See Less
|
/// See Less
|
||||||
internal static let seeless = L10n.tr("Localizable", "Scene.ServerPicker.Button.Seeless")
|
internal static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess")
|
||||||
/// See More
|
/// See More
|
||||||
internal static let seemore = L10n.tr("Localizable", "Scene.ServerPicker.Button.Seemore")
|
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
|
||||||
internal enum Category {
|
internal enum Category {
|
||||||
/// All
|
/// All
|
||||||
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")
|
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.784",
|
"blue" : "140",
|
||||||
"green" : "0.682",
|
"green" : "130",
|
||||||
"red" : "0.608"
|
"red" : "110"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "217",
|
||||||
|
"green" : "144",
|
||||||
|
"red" : "43"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
|
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
|
||||||
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
||||||
|
"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content.";
|
||||||
|
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
|
||||||
"Common.Alerts.ServerError.Title" = "Server Error";
|
"Common.Alerts.ServerError.Title" = "Server Error";
|
||||||
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
||||||
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
||||||
|
@ -9,6 +11,7 @@
|
||||||
"Common.Controls.Actions.Cancel" = "Cancel";
|
"Common.Controls.Actions.Cancel" = "Cancel";
|
||||||
"Common.Controls.Actions.Confirm" = "Confirm";
|
"Common.Controls.Actions.Confirm" = "Confirm";
|
||||||
"Common.Controls.Actions.Continue" = "Continue";
|
"Common.Controls.Actions.Continue" = "Continue";
|
||||||
|
"Common.Controls.Actions.Discard" = "Discard";
|
||||||
"Common.Controls.Actions.Edit" = "Edit";
|
"Common.Controls.Actions.Edit" = "Edit";
|
||||||
"Common.Controls.Actions.Ok" = "OK";
|
"Common.Controls.Actions.Ok" = "OK";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
|
@ -34,6 +37,10 @@
|
||||||
"Common.Controls.Timeline.LoadMore" = "Load More";
|
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||||
"Common.Countable.Photo.Multiple" = "photos";
|
"Common.Countable.Photo.Multiple" = "photos";
|
||||||
"Common.Countable.Photo.Single" = "photo";
|
"Common.Countable.Photo.Single" = "photo";
|
||||||
|
"Scene.Compose.ComposeAction" = "Publish";
|
||||||
|
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||||
|
"Scene.Compose.Title.NewPost" = "New Post";
|
||||||
|
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||||
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
||||||
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
|
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
|
||||||
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";
|
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";
|
||||||
|
@ -81,8 +88,8 @@ tap the link to confirm your account.";
|
||||||
"Scene.Register.Input.Username.Placeholder" = "username";
|
"Scene.Register.Input.Username.Placeholder" = "username";
|
||||||
"Scene.Register.Title" = "Tell us about you.";
|
"Scene.Register.Title" = "Tell us about you.";
|
||||||
"Scene.ServerPicker.Button.Category.All" = "All";
|
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||||
"Scene.ServerPicker.Button.Seeless" = "See Less";
|
"Scene.ServerPicker.Button.SeeLess" = "See Less";
|
||||||
"Scene.ServerPicker.Button.Seemore" = "See More";
|
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||||
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
||||||
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
||||||
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
||||||
|
|
|
@ -0,0 +1,448 @@
|
||||||
|
//
|
||||||
|
// ComposeViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import TwitterTextEditor
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: ComposeViewModel!
|
||||||
|
|
||||||
|
private var suffixedAttachmentViews: [UIView] = []
|
||||||
|
|
||||||
|
let composeTootBarButtonItem: UIBarButtonItem = {
|
||||||
|
let button = RoundedEdgesButton(type: .custom)
|
||||||
|
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
||||||
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color), for: .normal)
|
||||||
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||||
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||||
|
button.setTitleColor(.white, for: .normal)
|
||||||
|
button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
|
||||||
|
button.adjustsImageWhenHighlighted = false
|
||||||
|
let barButtonItem = UIBarButtonItem(customView: button)
|
||||||
|
return barButtonItem
|
||||||
|
}()
|
||||||
|
|
||||||
|
let tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.register(ComposeRepliedToTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self))
|
||||||
|
tableView.register(ComposeTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeTootContentTableViewCell.self))
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let composeToolbarView: ComposeToolbarView = {
|
||||||
|
let composeToolbarView = ComposeToolbarView()
|
||||||
|
composeToolbarView.backgroundColor = .secondarySystemBackground
|
||||||
|
return composeToolbarView
|
||||||
|
}()
|
||||||
|
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
|
||||||
|
let composeToolbarBackgroundView: UIView = {
|
||||||
|
let backgroundView = UIView()
|
||||||
|
backgroundView.backgroundColor = .secondarySystemBackground
|
||||||
|
return backgroundView
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
viewModel.title
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] title in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||||
|
navigationItem.rightBarButtonItem = composeTootBarButtonItem
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(composeToolbarView)
|
||||||
|
composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
composeToolbarViewBottomLayoutConstraint,
|
||||||
|
composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight),
|
||||||
|
])
|
||||||
|
composeToolbarView.preservesSuperviewLayoutMargins = true
|
||||||
|
composeToolbarView.delegate = self
|
||||||
|
|
||||||
|
composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
|
||||||
|
composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
|
||||||
|
composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.setupDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: self,
|
||||||
|
textEditorViewTextAttributesDelegate: self
|
||||||
|
)
|
||||||
|
|
||||||
|
// respond scrollView overlap change
|
||||||
|
view.layoutIfNeeded()
|
||||||
|
// update layout when keyboard show/dismiss
|
||||||
|
Publishers.CombineLatest3(
|
||||||
|
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
|
||||||
|
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
|
||||||
|
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.sink(receiveValue: { [weak self] isShow, state, endFrame in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
guard isShow, state == .dock else {
|
||||||
|
self.tableView.contentInset.bottom = 0.0
|
||||||
|
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
|
||||||
|
UIView.animate(withDuration: 0.3) {
|
||||||
|
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// isShow AND dock state
|
||||||
|
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
|
||||||
|
let padding = contentFrame.maxY - endFrame.minY
|
||||||
|
guard padding > 0 else {
|
||||||
|
self.tableView.contentInset.bottom = 0.0
|
||||||
|
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
|
||||||
|
UIView.animate(withDuration: 0.3) {
|
||||||
|
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add 16pt margin
|
||||||
|
self.tableView.contentInset.bottom = padding + 16
|
||||||
|
self.tableView.verticalScrollIndicatorInsets.bottom = padding + 16
|
||||||
|
UIView.animate(withDuration: 0.3) {
|
||||||
|
self.composeToolbarViewBottomLayoutConstraint.constant = padding
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.isComposeTootBarButtonItemEnabled
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.isEnabled, on: composeTootBarButtonItem)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind custom emojis
|
||||||
|
viewModel.customEmojiViewModel
|
||||||
|
.compactMap { $0?.emojis }
|
||||||
|
.switchToLatest()
|
||||||
|
.sink(receiveValue: { [weak self] emojis in
|
||||||
|
guard let self = self else { return }
|
||||||
|
for emoji in emojis {
|
||||||
|
UITextChecker.learnWord(emoji.shortcode)
|
||||||
|
UITextChecker.learnWord(":" + emoji.shortcode + ":")
|
||||||
|
}
|
||||||
|
self.textEditorView()?.setNeedsUpdateTextAttributes()
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
// Fix AutoLayout conflict issue
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.markTextEditorViewBecomeFirstResponser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeViewController {
|
||||||
|
|
||||||
|
private func textEditorView() -> TextEditorView? {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||||
|
let items = diffableDataSource.snapshot().itemIdentifiers
|
||||||
|
for item in items {
|
||||||
|
switch item {
|
||||||
|
case .input:
|
||||||
|
guard let indexPath = diffableDataSource.indexPath(for: item),
|
||||||
|
let cell = tableView.cellForRow(at: indexPath) as? ComposeTootContentTableViewCell else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return cell.textEditorView
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markTextEditorViewBecomeFirstResponser() {
|
||||||
|
textEditorView()?.isEditing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showDismissConfirmAlertController() {
|
||||||
|
let alertController = UIAlertController(
|
||||||
|
title: L10n.Common.Alerts.DiscardPostContent.title,
|
||||||
|
message: L10n.Common.Alerts.DiscardPostContent.message,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
alertController.addAction(discardAction)
|
||||||
|
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeViewController {
|
||||||
|
|
||||||
|
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
guard viewModel.shouldDismiss.value else {
|
||||||
|
showDismissConfirmAlertController()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TextEditorViewTextAttributesDelegate
|
||||||
|
extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
|
|
||||||
|
func textEditorView(
|
||||||
|
_ textEditorView: TextEditorView,
|
||||||
|
updateAttributedString attributedString: NSAttributedString,
|
||||||
|
completion: @escaping (NSAttributedString?) -> Void
|
||||||
|
) {
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
let string = attributedString.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 highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)|#([^\\s.]+))")
|
||||||
|
// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
||||||
|
// precondition :\B with following space
|
||||||
|
let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
|
||||||
|
// only accept http/https scheme
|
||||||
|
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let customEmojiViewModel = self.viewModel.customEmojiViewModel.value
|
||||||
|
for view in self.suffixedAttachmentViews {
|
||||||
|
view.removeFromSuperview()
|
||||||
|
}
|
||||||
|
self.suffixedAttachmentViews.removeAll()
|
||||||
|
|
||||||
|
// set normal apperance
|
||||||
|
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||||
|
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
|
||||||
|
attributedString.removeAttribute(.underlineStyle, range: stringRange)
|
||||||
|
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
|
||||||
|
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
|
||||||
|
|
||||||
|
for match in highlightMatches {
|
||||||
|
// hashtag
|
||||||
|
if let name = string.substring(with: match, at: 2) {
|
||||||
|
let attachment: TextAttributes.SuffixedAttachment?
|
||||||
|
switch name {
|
||||||
|
// FIXME:
|
||||||
|
case "person":
|
||||||
|
attachment = .init(size: CGSize(width: 20.0, height: 20.0),
|
||||||
|
attachment: .image(UIImage(systemName: "person")!))
|
||||||
|
default:
|
||||||
|
attachment = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let attachment = attachment {
|
||||||
|
let index = match.range.upperBound - 1
|
||||||
|
attributedString.addAttribute(
|
||||||
|
.suffixedAttachment,
|
||||||
|
value: attachment,
|
||||||
|
range: NSRange(location: index, length: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set highlight
|
||||||
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
|
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
||||||
|
// See `traitCollectionDidChange(_:)`
|
||||||
|
// set accessibility
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
switch self.traitCollection.accessibilityContrast {
|
||||||
|
case .high:
|
||||||
|
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
|
}
|
||||||
|
|
||||||
|
let emojis = customEmojiViewModel?.emojis.value ?? []
|
||||||
|
if !emojis.isEmpty {
|
||||||
|
for match in emojiMatches {
|
||||||
|
guard let name = string.substring(with: match, at: 2) else { continue }
|
||||||
|
guard let emoji = emojis.first(where: { $0.shortcode == name }) else { continue }
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
||||||
|
|
||||||
|
// set emoji token invisiable (without upper bounce space)
|
||||||
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
|
attributes[.font] = UIFont.systemFont(ofSize: 0.01)
|
||||||
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
|
|
||||||
|
// append emoji attachment
|
||||||
|
let imageViewSize = CGSize(width: 20, height: 20)
|
||||||
|
let imageView = UIImageView(frame: CGRect(origin: .zero, size: imageViewSize))
|
||||||
|
textEditorView.textContentView.addSubview(imageView)
|
||||||
|
self.suffixedAttachmentViews.append(imageView)
|
||||||
|
let processor = DownsamplingImageProcessor(size: imageViewSize)
|
||||||
|
imageView.kf.setImage(
|
||||||
|
with: URL(string: emoji.url),
|
||||||
|
placeholder: UIImage.placeholder(size: imageViewSize, color: .systemFill),
|
||||||
|
options: [
|
||||||
|
.processor(processor),
|
||||||
|
.scaleFactor(textEditorView.traitCollection.displayScale),
|
||||||
|
], completionHandler: nil
|
||||||
|
)
|
||||||
|
let layoutInTextContainer = { [weak textEditorView] (view: UIView, frame: CGRect) in
|
||||||
|
// `textEditorView` retains `textStorage`, which retains this block as a part of attributes.
|
||||||
|
guard let textEditorView = textEditorView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let insets = textEditorView.textContentInsets
|
||||||
|
view.frame = frame.offsetBy(dx: insets.left, dy: insets.top)
|
||||||
|
}
|
||||||
|
let attachment = TextAttributes.SuffixedAttachment(
|
||||||
|
size: imageViewSize,
|
||||||
|
attachment: .view(view: imageView, layoutInTextContainer: layoutInTextContainer)
|
||||||
|
)
|
||||||
|
let index = match.range.upperBound - 1
|
||||||
|
attributedString.addAttribute(
|
||||||
|
.suffixedAttachment,
|
||||||
|
value: attachment,
|
||||||
|
range: NSRange(location: index, length: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for match in urlMatches {
|
||||||
|
if let name = string.substring(with: match, at: 0) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
||||||
|
|
||||||
|
// set highlight
|
||||||
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
|
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
||||||
|
// See `traitCollectionDidChange(_:)`
|
||||||
|
// set accessibility
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
switch self.traitCollection.accessibilityContrast {
|
||||||
|
case .high:
|
||||||
|
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(attributedString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension ComposeViewController: UITableViewDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
|
return UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||||
|
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
||||||
|
|
||||||
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||||
|
return viewModel.shouldDismiss.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
showDismissConfirmAlertController()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// ComposeViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import TwitterTextEditor
|
||||||
|
|
||||||
|
extension ComposeViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
dependency: NeedsDependency,
|
||||||
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
|
||||||
|
) {
|
||||||
|
diffableDataSource = ComposeStatusSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: dependency,
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
composeKind: composeKind,
|
||||||
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate
|
||||||
|
)
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||||
|
snapshot.appendSections([.repliedTo, .status])
|
||||||
|
switch composeKind {
|
||||||
|
case .reply(let statusObjectID):
|
||||||
|
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
||||||
|
snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo)
|
||||||
|
case .post:
|
||||||
|
snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status)
|
||||||
|
}
|
||||||
|
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// ComposeViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
final class ComposeViewModel {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let composeKind: ComposeStatusSection.ComposeKind
|
||||||
|
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||||
|
let composeContent = CurrentValueSubject<String, Never>("")
|
||||||
|
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
|
|
||||||
|
// UI & UX
|
||||||
|
let title: CurrentValueSubject<String, Never>
|
||||||
|
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
let isComposeTootBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
// custom emojis
|
||||||
|
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||||
|
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AppContext,
|
||||||
|
composeKind: ComposeStatusSection.ComposeKind
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.composeKind = composeKind
|
||||||
|
switch composeKind {
|
||||||
|
case .post: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newPost)
|
||||||
|
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
|
||||||
|
}
|
||||||
|
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||||
|
// end init
|
||||||
|
|
||||||
|
// bind active authentication
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.assign(to: \.value, on: activeAuthentication)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind avatar and names
|
||||||
|
activeAuthentication
|
||||||
|
.sink { [weak self] mastodonAuthentication in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let mastodonUser = mastodonAuthentication?.user
|
||||||
|
let username = mastodonUser?.username ?? " "
|
||||||
|
|
||||||
|
self.composeStatusAttribute.avatarURL.value = mastodonUser?.avatarImageURL()
|
||||||
|
self.composeStatusAttribute.displayName.value = {
|
||||||
|
guard let displayName = mastodonUser?.displayName, !displayName.isEmpty else {
|
||||||
|
return username
|
||||||
|
}
|
||||||
|
return displayName
|
||||||
|
}()
|
||||||
|
self.composeStatusAttribute.username.value = username
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind compose bar button item UI state
|
||||||
|
composeStatusAttribute.composeContent
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.map { content in
|
||||||
|
let content = content?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
return !content.isEmpty
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: isComposeTootBarButtonItemEnabled)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind modal dismiss state
|
||||||
|
composeStatusAttribute.composeContent
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.map { content in
|
||||||
|
let content = content ?? ""
|
||||||
|
return content.isEmpty
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: shouldDismiss)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind custom emojis
|
||||||
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] activeMastodonAuthenticationBox in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return }
|
||||||
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
|
||||||
|
// trigger dequeue to preload emojis
|
||||||
|
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// ComposeRepliedToTootContentTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ComposeRepliedToTootContentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeRepliedToTootContentTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// ComposeTootContentTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import TwitterTextEditor
|
||||||
|
|
||||||
|
final class ComposeTootContentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let statusView = StatusView()
|
||||||
|
|
||||||
|
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
|
||||||
|
}()
|
||||||
|
|
||||||
|
let composeContent = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeTootContentTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
selectionStyle = .none
|
||||||
|
|
||||||
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(statusView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, 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
|
||||||
|
|
||||||
|
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(textEditorView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||||
|
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
|
||||||
|
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
|
||||||
|
textEditorView.changeObserver = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMoveToWindow() {
|
||||||
|
super.didMoveToWindow()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeTootContentTableViewCell {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension ComposeTootContentTableViewCell: TextEditorViewChangeObserver {
|
||||||
|
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||||
|
guard changeResult.isTextChanged else { return }
|
||||||
|
composeContent.send(textEditorView.text)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
//
|
||||||
|
// ComposeToolbarView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ComposeToolbarViewDelegate: class {
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton)
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
|
||||||
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComposeToolbarView: UIView {
|
||||||
|
|
||||||
|
static let toolbarHeight: CGFloat = 44
|
||||||
|
|
||||||
|
weak var delegate: ComposeToolbarViewDelegate?
|
||||||
|
|
||||||
|
let mediaButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
let pollButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
let emojiButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
let contentWarningButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
let visibilityButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeToolbarView {
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = .secondarySystemBackground
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.spacing = 0
|
||||||
|
stackView.distribution = .fillEqually
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
layoutMarginsGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 8), // tweak button margin offset
|
||||||
|
])
|
||||||
|
|
||||||
|
let buttons = [
|
||||||
|
mediaButton,
|
||||||
|
pollButton,
|
||||||
|
emojiButton,
|
||||||
|
contentWarningButton,
|
||||||
|
visibilityButton,
|
||||||
|
]
|
||||||
|
buttons.forEach { button in
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(button)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
button.widthAnchor.constraint(equalToConstant: 44),
|
||||||
|
button.heightAnchor.constraint(equalToConstant: 44),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaButton.addTarget(self, action: #selector(ComposeToolbarView.cameraButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.locationButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension ComposeToolbarView {
|
||||||
|
|
||||||
|
@objc private func cameraButtonDidPressed(_ sender: UIButton) {
|
||||||
|
delegate?.composeToolbarView(self, cameraButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func gifButtonDidPressed(_ sender: UIButton) {
|
||||||
|
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func atButtonDidPressed(_ sender: UIButton) {
|
||||||
|
delegate?.composeToolbarView(self, atButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func topicButtonDidPressed(_ sender: UIButton) {
|
||||||
|
delegate?.composeToolbarView(self, topicButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func locationButtonDidPressed(_ sender: UIButton) {
|
||||||
|
delegate?.composeToolbarView(self, locationButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ComposeToolbarView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let tootbarView = ComposeToolbarView()
|
||||||
|
tootbarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tootbarView.widthAnchor.constraint(equalToConstant: 375).priority(.defaultHigh),
|
||||||
|
tootbarView.heightAnchor.constraint(equalToConstant: 64).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
return tootbarView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -84,9 +84,9 @@ extension HomeTimelineViewController {
|
||||||
identifier: nil,
|
identifier: nil,
|
||||||
options: [],
|
options: [],
|
||||||
children: [50, 100, 150, 200, 250, 300].map { count in
|
children: [50, 100, 150, 200, 250, 300].map { count in
|
||||||
UIAction(title: "Drop Recent \(count) Toots", image: nil, attributes: [], handler: { [weak self] action in
|
UIAction(title: "Drop Recent \(count) Statuses", image: nil, attributes: [], handler: { [weak self] action in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.dropRecentTootsAction(action, count: count)
|
self.dropRecentStatusAction(action, count: count)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -136,8 +136,8 @@ extension HomeTimelineViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .homeTimelineIndex(let objectID, _):
|
case .homeTimelineIndex(let objectID, _):
|
||||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
let post = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
||||||
return toot.poll != nil
|
return post.poll != nil
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ extension HomeTimelineViewController {
|
||||||
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||||
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||||
} else {
|
} else {
|
||||||
print("Not found poll toot")
|
print("Not found poll status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ extension HomeTimelineViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) {
|
@objc private func dropRecentStatusAction(_ sender: UIAction, count: Int) {
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||||
|
|
||||||
|
@ -197,8 +197,8 @@ extension HomeTimelineViewController {
|
||||||
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
for objectID in droppingTootObjectIDs {
|
for objectID in droppingTootObjectIDs {
|
||||||
guard let toot = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
|
guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
|
||||||
self.context.apiService.backgroundManagedObjectContext.delete(toot)
|
self.context.apiService.backgroundManagedObjectContext.delete(post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
|
|
|
@ -164,7 +164,8 @@ extension HomeTimelineViewController {
|
||||||
|
|
||||||
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
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)
|
||||||
|
let composeViewModel = ComposeViewModel(context: context, composeKind: .post)
|
||||||
|
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
|
|
@ -88,8 +88,8 @@ class PickServerCell: UITableViewCell {
|
||||||
|
|
||||||
let expandButton: UIButton = {
|
let expandButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = UIButton(type: .custom)
|
||||||
button.setTitle(L10n.Scene.ServerPicker.Button.seemore, for: .normal)
|
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeless, for: .selected)
|
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
|
||||||
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
||||||
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -351,7 +351,7 @@ extension MastodonRegisterViewController {
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
|
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
|
||||||
KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher()
|
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
|
||||||
)
|
)
|
||||||
.sink(receiveValue: { [weak self] state, endFrame in
|
.sink(receiveValue: { [weak self] state, endFrame in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
|
@ -217,6 +217,7 @@ extension WelcomeViewController: OnboardingViewControllerAppearance { }
|
||||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||||
extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
|
extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
|
||||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||||
return .fullScreen
|
// make underneath view controller alive to fix layout issue due to view life cycle
|
||||||
|
return .overFullScreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// RoundedEdgesButton.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class RoundedEdgesButton: UIButton {
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
layer.masksToBounds = true
|
||||||
|
layer.cornerRadius = bounds.height * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -71,6 +71,14 @@ final class StatusView: UIView {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let nameTrialingDotLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = .systemFont(ofSize: 17)
|
||||||
|
label.text = "·"
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
let usernameLabel: UILabel = {
|
let usernameLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
@ -268,18 +276,11 @@ extension StatusView {
|
||||||
nameLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
|
nameLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
titleContainerStackView.alignment = .firstBaseline
|
titleContainerStackView.alignment = .firstBaseline
|
||||||
let dotLabel: UILabel = {
|
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
|
||||||
let label = UILabel()
|
|
||||||
label.textColor = Asset.Colors.Label.secondary.color
|
|
||||||
label.font = .systemFont(ofSize: 17)
|
|
||||||
label.text = "·"
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
titleContainerStackView.addArrangedSubview(dotLabel)
|
|
||||||
titleContainerStackView.addArrangedSubview(dateLabel)
|
titleContainerStackView.addArrangedSubview(dateLabel)
|
||||||
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||||
dotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
||||||
dotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||||
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
|
|
||||||
|
@ -360,7 +361,7 @@ extension StatusView {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
audioView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor),
|
audioView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor),
|
||||||
audioView.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
|
audioView.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
|
||||||
audioView.heightAnchor.constraint(equalToConstant: 44)
|
audioView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
|
||||||
])
|
])
|
||||||
// video gif
|
// video gif
|
||||||
statusContainerStackView.addArrangedSubview(playerContainerView)
|
statusContainerStackView.addArrangedSubview(playerContainerView)
|
||||||
|
|
|
@ -20,7 +20,6 @@ protocol StatusTableViewCellDelegate: class {
|
||||||
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
|
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||||
|
|
||||||
|
|
||||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||||
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)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// APIService+CustomEmojiViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
import DateToolsSwift
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func customEmoji(domain: String) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Emoji]>, Error> {
|
||||||
|
return Mastodon.API.CustomEmojis.customEmojis(session: session, domain: domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
class AuthenticationService: NSObject {
|
final class AuthenticationService: NSObject {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
// input
|
// input
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// EmojiService+CustomEmojiViewModel+LoadState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
|
extension EmojiService.CustomEmojiViewModel {
|
||||||
|
class LoadState: GKState {
|
||||||
|
weak var viewModel: EmojiService.CustomEmojiViewModel?
|
||||||
|
|
||||||
|
init(viewModel: EmojiService.CustomEmojiViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EmojiService.CustomEmojiViewModel.LoadState {
|
||||||
|
|
||||||
|
class Initial: EmojiService.CustomEmojiViewModel.LoadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: EmojiService.CustomEmojiViewModel.LoadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Fail.self || stateClass == Finish.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let apiService = viewModel.service?.apiService, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
apiService.customEmoji(domain: viewModel.domain)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to load custom emojis for %s: %s. Retry 10s later", ((#file as NSString).lastPathComponent), #line, #function, viewModel.domain, error.localizedDescription)
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: load %ld custom emojis for %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.count, viewModel.domain)
|
||||||
|
stateMachine.enter(Finish.self)
|
||||||
|
viewModel.emojis.value = response.value
|
||||||
|
}
|
||||||
|
.store(in: &viewModel.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: EmojiService.CustomEmojiViewModel.LoadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Loading.self || stateClass == Finish.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
|
guard let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
// retry 10s later
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
|
||||||
|
stateMachine.enter(Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Finish: EmojiService.CustomEmojiViewModel.LoadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// one time task
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
//
|
||||||
|
// EmojiService+CustomEmojiViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension EmojiService {
|
||||||
|
final class CustomEmojiViewModel {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let domain: String
|
||||||
|
weak var service: EmojiService?
|
||||||
|
|
||||||
|
// output
|
||||||
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
LoadState.Initial(viewModel: self),
|
||||||
|
LoadState.Loading(viewModel: self),
|
||||||
|
LoadState.Fail(viewModel: self),
|
||||||
|
LoadState.Finish(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(LoadState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([])
|
||||||
|
|
||||||
|
init(domain: String, service: EmojiService) {
|
||||||
|
self.domain = domain
|
||||||
|
self.service = service
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// EmojiService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class EmojiService {
|
||||||
|
|
||||||
|
|
||||||
|
weak var apiService: APIService?
|
||||||
|
|
||||||
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.EmojiService.working-queue")
|
||||||
|
private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:]
|
||||||
|
|
||||||
|
init(apiService: APIService) {
|
||||||
|
self.apiService = apiService
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EmojiService {
|
||||||
|
|
||||||
|
func dequeueCustomEmojiViewModel(for domain: String) -> CustomEmojiViewModel? {
|
||||||
|
var _customEmojiViewModel: CustomEmojiViewModel?
|
||||||
|
workingQueue.sync {
|
||||||
|
if let viewModel = customEmojiViewModelDict[domain] {
|
||||||
|
_customEmojiViewModel = viewModel
|
||||||
|
} else {
|
||||||
|
let viewModel = CustomEmojiViewModel(domain: domain, service: self)
|
||||||
|
_customEmojiViewModel = viewModel
|
||||||
|
|
||||||
|
// trigger loading
|
||||||
|
viewModel.stateMachine.enter(CustomEmojiViewModel.LoadState.Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _customEmojiViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -18,9 +18,8 @@ final class KeyboardResponderService {
|
||||||
// output
|
// output
|
||||||
let isShow = CurrentValueSubject<Bool, Never>(false)
|
let isShow = CurrentValueSubject<Bool, Never>(false)
|
||||||
let state = CurrentValueSubject<KeyboardState, Never>(.none)
|
let state = CurrentValueSubject<KeyboardState, Never>(.none)
|
||||||
let didEndFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
let endFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
let willEndFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil)
|
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil)
|
||||||
.sink { notification in
|
.sink { notification in
|
||||||
|
@ -38,15 +37,11 @@ final class KeyboardResponderService {
|
||||||
|
|
||||||
NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil)
|
NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil)
|
||||||
.sink { notification in
|
.sink { notification in
|
||||||
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
||||||
self.didEndFrame.value = endFrame
|
|
||||||
self.updateInternalStatus(notification: notification)
|
self.updateInternalStatus(notification: notification)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||||
.sink { notification in
|
.sink { notification in
|
||||||
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
||||||
self.willEndFrame.value = endFrame
|
|
||||||
self.updateInternalStatus(notification: notification)
|
self.updateInternalStatus(notification: notification)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
@ -62,6 +57,8 @@ extension KeyboardResponderService {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.endFrame.value = endFrame
|
||||||
|
|
||||||
guard isLocal else {
|
guard isLocal else {
|
||||||
self.state.value = .notLocal
|
self.state.value = .notLocal
|
||||||
return
|
return
|
||||||
|
|
|
@ -23,13 +23,13 @@ class AppContext: ObservableObject {
|
||||||
|
|
||||||
let apiService: APIService
|
let apiService: APIService
|
||||||
let authenticationService: AuthenticationService
|
let authenticationService: AuthenticationService
|
||||||
|
let emojiService: EmojiService
|
||||||
|
let audioPlaybackService = AudioPlaybackService()
|
||||||
|
let videoPlaybackService = VideoPlaybackService()
|
||||||
|
|
||||||
let documentStore: DocumentStore
|
let documentStore: DocumentStore
|
||||||
private var documentStoreSubscription: AnyCancellable!
|
private var documentStoreSubscription: AnyCancellable!
|
||||||
|
|
||||||
let videoPlaybackService = VideoPlaybackService()
|
|
||||||
let audioPlaybackService = AudioPlaybackService()
|
|
||||||
|
|
||||||
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
|
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -49,6 +49,10 @@ class AppContext: ObservableObject {
|
||||||
apiService: _apiService
|
apiService: _apiService
|
||||||
)
|
)
|
||||||
|
|
||||||
|
emojiService = EmojiService(
|
||||||
|
apiService: apiService
|
||||||
|
)
|
||||||
|
|
||||||
documentStore = DocumentStore()
|
documentStore = DocumentStore()
|
||||||
documentStoreSubscription = documentStore.objectWillChange
|
documentStoreSubscription = documentStore.objectWillChange
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// String.swift
|
||||||
|
// Example
|
||||||
|
//
|
||||||
|
// Copyright 2021 Twitter, Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
@inlinable
|
||||||
|
var length: Int {
|
||||||
|
(self as NSString).length
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func substring(with range: NSRange) -> String {
|
||||||
|
(self as NSString).substring(with: range)
|
||||||
|
}
|
||||||
|
|
||||||
|
func substring(with result: NSTextCheckingResult, at index: Int) -> String? {
|
||||||
|
guard index < result.numberOfRanges else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let range = result.range(at: index)
|
||||||
|
guard range.location != NSNotFound else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return substring(with: result.range(at: index))
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstMatch(pattern: String,
|
||||||
|
options: NSRegularExpression.Options = [],
|
||||||
|
range: NSRange? = nil) -> NSTextCheckingResult?
|
||||||
|
{
|
||||||
|
guard let regularExpression = try? NSRegularExpression(pattern: pattern, options: options) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let range = range ?? NSRange(location: 0, length: length)
|
||||||
|
return regularExpression.firstMatch(in: self, options: [], range: range)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matches(pattern: String,
|
||||||
|
options: NSRegularExpression.Options = [],
|
||||||
|
range: NSRange? = nil) -> [NSTextCheckingResult]
|
||||||
|
{
|
||||||
|
guard let regularExpression = try? NSRegularExpression(pattern: pattern, options: options) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let range = range ?? NSRange(location: 0, length: length)
|
||||||
|
return regularExpression.matches(in: self, options: [], range: range)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+CustomEmojis.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Mastodon.API.CustomEmojis {
|
||||||
|
|
||||||
|
static func customEmojisEndpointURL(domain: String) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("custom_emojis")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom emoji
|
||||||
|
///
|
||||||
|
/// Returns custom emojis that are available on the server.
|
||||||
|
///
|
||||||
|
/// - Since: 2.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/15
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/instance/custom_emojis/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - Returns: `AnyPublisher` contains [`Emoji`] nested in the response
|
||||||
|
public static func customEmojis(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Emoji]>, Error> {
|
||||||
|
let request = Mastodon.API.get(
|
||||||
|
url: customEmojisEndpointURL(domain: domain),
|
||||||
|
query: nil,
|
||||||
|
authorization: nil
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Emoji].self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
//
|
|
||||||
// Mastodon+API+Status.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-3-9.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension Mastodon.API.Status { }
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Statuses.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
|
@ -91,14 +91,15 @@ extension Mastodon.API {
|
||||||
extension Mastodon.API {
|
extension Mastodon.API {
|
||||||
public enum Account { }
|
public enum Account { }
|
||||||
public enum App { }
|
public enum App { }
|
||||||
|
public enum CustomEmojis { }
|
||||||
|
public enum Favorites { }
|
||||||
public enum Instance { }
|
public enum Instance { }
|
||||||
public enum OAuth { }
|
public enum OAuth { }
|
||||||
public enum Onboarding { }
|
public enum Onboarding { }
|
||||||
public enum Polls { }
|
public enum Polls { }
|
||||||
|
public enum Statuses { }
|
||||||
public enum Reblog { }
|
public enum Reblog { }
|
||||||
public enum Status { }
|
|
||||||
public enum Timeline { }
|
public enum Timeline { }
|
||||||
public enum Favorites { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.API {
|
extension Mastodon.API {
|
||||||
|
|
|
@ -53,5 +53,6 @@ arch -x86_64 pod install
|
||||||
- [Kingfisher](https://github.com/onevcat/Kingfisher)
|
- [Kingfisher](https://github.com/onevcat/Kingfisher)
|
||||||
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
||||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||||
|
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
Loading…
Reference in New Issue