feat: complete upload and publish logic

This commit is contained in:
CMK 2021-07-20 16:40:04 +08:00
parent 1cdbd7fa2a
commit d2f9828f50
59 changed files with 1003 additions and 425 deletions

View File

@ -22,7 +22,7 @@
"publish_post_failure": { "publish_post_failure": {
"title": "Publish Failure", "title": "Publish Failure",
"message": "Failed to publish the post.\nPlease check your internet connection.", "message": "Failed to publish the post.\nPlease check your internet connection.",
"attchments_message": { "attachments_message": {
"video_attach_with_photo": "Cannot attach a video to a post that already contains images.", "video_attach_with_photo": "Cannot attach a video to a post that already contains images.",
"more_than_one_video": "Cannot attach more than one video." "more_than_one_video": "Cannot attach more than one video."
} }

View File

@ -39,7 +39,6 @@
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; }; 2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; }; 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
@ -533,7 +532,6 @@
DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; }; DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; };
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; }; DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; };
DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B35261440BA0045B23D /* UINavigationController.swift */; }; DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B35261440BA0045B23D /* UINavigationController.swift */; };
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B88261454BA0045B23D /* CGImage.swift */; };
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; }; DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; };
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */; }; DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */; };
DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9A2615849F0045B23D /* PrivateNote.swift */; }; DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9A2615849F0045B23D /* PrivateNote.swift */; };
@ -569,6 +567,19 @@
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; }; DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; };
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; }; DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; };
DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; }; DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; };
DBFEF06826A67DEE006D7ED1 /* MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */; };
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; };
DBFEF06A26A67E53006D7ED1 /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
DBFEF06B26A67E58006D7ED1 /* Fields.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94439265CC0FC00C537E1 /* Fields.swift */; };
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; };
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
DBFEF07126A690E8006D7ED1 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */; };
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07226A6913D006D7ED1 /* APIService.swift */; };
DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; };
DBFEF07726A691FB006D7ED1 /* MastodonAuthenticationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */; };
DBFEF07826A69209006D7ED1 /* MastodonAuthenticationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */; };
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */; };
DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */; };
EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */; }; EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -727,7 +738,6 @@
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; }; 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
@ -1204,7 +1214,6 @@
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = "<group>"; }; DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = "<group>"; };
DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = "<group>"; }; DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = "<group>"; };
DBCC3B35261440BA0045B23D /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = "<group>"; }; DBCC3B35261440BA0045B23D /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = "<group>"; };
DBCC3B88261454BA0045B23D /* CGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImage.swift; sourceTree = "<group>"; };
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = "<group>"; }; DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = "<group>"; };
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Relationship.swift"; sourceTree = "<group>"; }; DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Relationship.swift"; sourceTree = "<group>"; };
DBCC3B9A2615849F0045B23D /* PrivateNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateNote.swift; sourceTree = "<group>"; }; DBCC3B9A2615849F0045B23D /* PrivateNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateNote.swift; sourceTree = "<group>"; };
@ -1242,6 +1251,10 @@
DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentView.swift; sourceTree = "<group>"; }; DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentView.swift; sourceTree = "<group>"; };
DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentViewModel.swift; sourceTree = "<group>"; }; DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentViewModel.swift; sourceTree = "<group>"; };
DBFEF06726A58D07006D7ED1 /* ShareActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareActionExtension.entitlements; sourceTree = "<group>"; }; DBFEF06726A58D07006D7ED1 /* ShareActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareActionExtension.entitlements; sourceTree = "<group>"; };
DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusAttachmentViewModel+UploadState.swift"; sourceTree = "<group>"; };
DBFEF07226A6913D006D7ED1 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = "<group>"; };
DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status+Publish.swift"; sourceTree = "<group>"; };
DDB1B139FA8EA26F510D58B6 /* Pods-AppShared.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.asdk - release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.asdk - release.xcconfig"; sourceTree = "<group>"; }; DDB1B139FA8EA26F510D58B6 /* Pods-AppShared.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.asdk - release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.asdk - release.xcconfig"; sourceTree = "<group>"; };
E5C7236E58D14A0322FE00F2 /* Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; sourceTree = "<group>"; }; E5C7236E58D14A0322FE00F2 /* Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; sourceTree = "<group>"; };
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
@ -1341,6 +1354,7 @@
DB41ED8026A54D7C00F58330 /* AlamofireImage in Frameworks */, DB41ED8026A54D7C00F58330 /* AlamofireImage in Frameworks */,
DBC6463326A195DB00B0E31B /* AppShared.framework in Frameworks */, DBC6463326A195DB00B0E31B /* AppShared.framework in Frameworks */,
4278334D6033AEEE0A1C5155 /* Pods_ShareActionExtension.framework in Frameworks */, 4278334D6033AEEE0A1C5155 /* Pods_ShareActionExtension.framework in Frameworks */,
DBFEF07126A690E8006D7ED1 /* AlamofireNetworkActivityIndicator in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -2055,6 +2069,7 @@
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */, DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */, DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */, DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */,
DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */, DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */,
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */, DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
2D61254C262547C200299647 /* APIService+Notification.swift */, 2D61254C262547C200299647 /* APIService+Notification.swift */,
@ -2497,8 +2512,6 @@
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */, DBD376B1269302A4007FEC24 /* UITableViewCell.swift */,
0FAA101B25E10E760017CCDE /* UIFont.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */,
2D939AB425EDD8A90076FA61 /* String.swift */, 2D939AB425EDD8A90076FA61 /* String.swift */,
2D206B7F25F5F45E00143C56 /* UIImage.swift */,
DBCC3B88261454BA0045B23D /* CGImage.swift */,
2D206B8525F5FB0900143C56 /* Double.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */,
2D206B9125F60EA700143C56 /* UIControl.swift */, 2D206B9125F60EA700143C56 /* UIControl.swift */,
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
@ -2764,6 +2777,7 @@
DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */, DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */,
DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */, DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */,
DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */, DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */,
DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */,
); );
path = Helper; path = Helper;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2791,6 +2805,7 @@
DBC6461926A170AB00B0E31B /* Info.plist */, DBC6461926A170AB00B0E31B /* Info.plist */,
DBC6461626A170AB00B0E31B /* MainInterface.storyboard */, DBC6461626A170AB00B0E31B /* MainInterface.storyboard */,
DBFEF06126A57721006D7ED1 /* Scene */, DBFEF06126A57721006D7ED1 /* Scene */,
DBFEF07426A69140006D7ED1 /* Service */,
); );
path = ShareActionExtension; path = ShareActionExtension;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2902,6 +2917,7 @@
DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */, DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */,
DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */, DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */,
DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */, DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */,
DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */,
); );
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2916,6 +2932,14 @@
path = Scene; path = Scene;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
DBFEF07426A69140006D7ED1 /* Service */ = {
isa = PBXGroup;
children = (
DBFEF07226A6913D006D7ED1 /* APIService.swift */,
);
path = Service;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */ /* Begin PBXHeadersBuildPhase section */
@ -3111,6 +3135,7 @@
DB41ED7F26A54D7C00F58330 /* AlamofireImage */, DB41ED7F26A54D7C00F58330 /* AlamofireImage */,
DB41ED8126A54D8A00F58330 /* MastodonMeta */, DB41ED8126A54D8A00F58330 /* MastodonMeta */,
DB41ED8326A54D8A00F58330 /* MetaTextView */, DB41ED8326A54D8A00F58330 /* MetaTextView */,
DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */,
); );
productName = ShareActionExtension; productName = ShareActionExtension;
productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */;
@ -3821,6 +3846,7 @@
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */, DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */,
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */,
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */, DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */, 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
@ -3836,7 +3862,6 @@
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */, 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */,
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */, 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */,
0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */, 0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */,
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */,
DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */, DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */,
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */, 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */, DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */,
@ -3871,6 +3896,7 @@
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */, DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
DBBC24B526A540AE00398BB9 /* AvatarConfigurableView.swift in Sources */, DBBC24B526A540AE00398BB9 /* AvatarConfigurableView.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */, DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
DBFEF07726A691FB006D7ED1 /* MastodonAuthenticationBox.swift in Sources */,
DBBC24CF26A547AE00398BB9 /* ThemeService+Appearance.swift in Sources */, DBBC24CF26A547AE00398BB9 /* ThemeService+Appearance.swift in Sources */,
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
@ -3910,7 +3936,6 @@
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */, 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */, DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */,
DBAFB7352645463500371D5F /* Emojis.swift in Sources */, DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */, DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */, 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
@ -3993,21 +4018,28 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */, DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */,
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */,
DB41ED7C26A54D5500F58330 /* MastodonStatusContent+Appearance.swift in Sources */, DB41ED7C26A54D5500F58330 /* MastodonStatusContent+Appearance.swift in Sources */,
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */, DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */,
DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */, DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */,
DBBC24B326A53EE700398BB9 /* ActiveLabel.swift in Sources */, DBBC24B326A53EE700398BB9 /* ActiveLabel.swift in Sources */,
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */,
DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */, DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */,
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */, DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */,
DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */, DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */,
DBFEF06A26A67E53006D7ED1 /* Emojis.swift in Sources */,
DBFEF07526A69192006D7ED1 /* APIService+Media.swift in Sources */,
DB41ED7B26A54D4D00F58330 /* MastodonStatusContent+ParseResult.swift in Sources */, DB41ED7B26A54D4D00F58330 /* MastodonStatusContent+ParseResult.swift in Sources */,
DB41ED8A26A54F4C00F58330 /* AttachmentContainerView.swift in Sources */, DB41ED8A26A54F4C00F58330 /* AttachmentContainerView.swift in Sources */,
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */,
DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */, DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */,
DBBC24C726A5456400398BB9 /* SystemTheme.swift in Sources */, DBBC24C726A5456400398BB9 /* SystemTheme.swift in Sources */,
DBBC24B626A5419700398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */, DBBC24B626A5419700398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */,
DBC6462926A1736700B0E31B /* Strings.swift in Sources */, DBC6462926A1736700B0E31B /* Strings.swift in Sources */,
DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */, DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */,
DBFEF06826A67DEE006D7ED1 /* MastodonUser.swift in Sources */,
DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */, DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */,
DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */,
DBBC24B926A5426000398BB9 /* StatusContentWarningEditorView.swift in Sources */, DBBC24B926A5426000398BB9 /* StatusContentWarningEditorView.swift in Sources */,
DB41ED8B26A54F5800F58330 /* AttachmentContainerView+EmptyStateView.swift in Sources */, DB41ED8B26A54F5800F58330 /* AttachmentContainerView+EmptyStateView.swift in Sources */,
DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */, DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */,
@ -4019,6 +4051,9 @@
DB41ED8926A54F4000F58330 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, DB41ED8926A54F4000F58330 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
DBBC24D226A5488600398BB9 /* AvatarConfigurableView.swift in Sources */, DBBC24D226A5488600398BB9 /* AvatarConfigurableView.swift in Sources */,
DBC6462C26A176B000B0E31B /* Assets.swift in Sources */, DBC6462C26A176B000B0E31B /* Assets.swift in Sources */,
DBFEF06926A67E45006D7ED1 /* AppearancePreference.swift in Sources */,
DBFEF06B26A67E58006D7ED1 /* Fields.swift in Sources */,
DBFEF07826A69209006D7ED1 /* MastodonAuthenticationBox.swift in Sources */,
DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */, DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -5579,6 +5614,11 @@
package = DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */; package = DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */;
productName = FPSIndicator; productName = FPSIndicator;
}; };
DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */ = {
isa = XCSwiftPackageProductDependency;
package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */;
productName = AlamofireNetworkActivityIndicator;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */

View File

@ -1,154 +0,0 @@
//
// CGImage.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-31.
//
import CoreImage
extension CGImage {
// Reference
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
// Luma Y = 0.2126R + 0.7152G + 0.0722B
var brightness: CGFloat? {
let context = CIContext() // default with metal accelerate
let ciImage = CIImage(cgImage: self)
let rec709Image = context.createCGImage(
ciImage,
from: ciImage.extent,
format: .RGBA8,
colorSpace: CGColorSpace(name: CGColorSpace.itur_709) // BT.709 a.k.a Rec.709
)
guard let image = rec709Image,
image.bitsPerPixel == 32,
let data = rec709Image?.dataProvider?.data,
let pointer = CFDataGetBytePtr(data) else { return nil }
let length = CFDataGetLength(data)
guard length > 0 else { return nil }
var luma: CGFloat = 0.0
for i in stride(from: 0, to: length, by: 4) {
let r = pointer[i]
let g = pointer[i + 1]
let b = pointer[i + 2]
let Y = 0.2126 * CGFloat(r) + 0.7152 * CGFloat(g) + 0.0722 * CGFloat(b)
luma += Y
}
luma /= CGFloat(width * height)
return luma
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
import UIKit
class BrightnessView: UIView {
let label = UILabel()
let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
stackView.distribution = .fillEqually
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(label)
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
label.textAlignment = .center
label.numberOfLines = 0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setImage(_ image: UIImage) {
imageView.image = image
guard let brightness = image.cgImage?.brightness,
let style = image.domainLumaCoefficientsStyle else {
label.text = "<nil>"
return
}
let styleDescription: String = {
switch style {
case .light: return "Light"
case .dark: return "Dark"
case .unspecified: fallthrough
@unknown default:
return "Unknown"
}
}()
label.text = styleDescription + "\n" + "\(brightness)"
}
}
struct CGImage_Brightness_Previews: PreviewProvider {
static var previews: some View {
Group {
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .black))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .gray))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .separator))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .red))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .green))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .blue))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
UIViewPreview(width: 375) {
let view = BrightnessView()
view.setImage(.placeholder(color: .secondarySystemGroupedBackground))
return view
}
.previewLayout(.fixed(width: 375, height: 44))
}
}
}
#endif

View File

@ -1,81 +0,0 @@
//
// UIImage.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/8.
//
import CoreImage
import CoreImage.CIFilterBuiltins
import UIKit
// refs: https://www.hackingwithswift.com/example-code/media/how-to-read-the-average-color-of-a-uiimage-using-ciareaaverage
extension UIImage {
@available(iOS 14.0, *)
var dominantColor: UIColor? {
guard let inputImage = CIImage(image: self) else { return nil }
let filter = CIFilter.areaAverage()
filter.inputImage = inputImage
filter.extent = inputImage.extent
guard let outputImage = filter.outputImage else { return nil }
var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}
extension UIImage {
var domainLumaCoefficientsStyle: UIUserInterfaceStyle? {
guard let brightness = cgImage?.brightness else { return nil }
return brightness > 100 ? .light : .dark // 0 ~ 255
}
}
extension UIImage {
func blur(radius: CGFloat) -> UIImage? {
guard let inputImage = CIImage(image: self) else { return nil }
let blurFilter = CIFilter.gaussianBlur()
blurFilter.inputImage = inputImage
blurFilter.radius = Float(radius)
guard let outputImage = blurFilter.outputImage else { return nil }
guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { return nil }
let image = UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation)
return image
}
}
extension UIImage {
func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? {
let maxRadius = min(size.width, size.height) / 2
let cornerRadius: CGFloat = {
guard let radius = radius, radius > 0 else { return maxRadius }
return min(radius, maxRadius)
}()
let render = UIGraphicsImageRenderer(size: size)
return render.image { (_: UIGraphicsImageRendererContext) in
let rect = CGRect(origin: .zero, size: size)
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
draw(in: rect)
}
}
}
extension UIImage {
static func adaptiveUserInterfaceStyleImage(lightImage: UIImage, darkImage: UIImage) -> UIImage {
let imageAsset = UIImageAsset()
imageAsset.register(lightImage, with: UITraitCollection(traitsFrom: [
UITraitCollection(displayScale: 1.0),
UITraitCollection(userInterfaceStyle: .light)
]))
imageAsset.register(darkImage, with: UITraitCollection(traitsFrom: [
UITraitCollection(displayScale: 1.0),
UITraitCollection(userInterfaceStyle: .dark)
]))
return imageAsset.image(with: UITraitCollection.current)
}
}

View File

@ -58,11 +58,11 @@ internal enum L10n {
internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message") internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message")
/// Publish Failure /// Publish Failure
internal static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title") internal static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title")
internal enum AttchmentsMessage { internal enum AttachmentsMessage {
/// Cannot attach more than one video. /// Cannot attach more than one video.
internal static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo") internal static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo")
/// Cannot attach a video to a post that already contains images. /// Cannot attach a video to a post that already contains images.
internal static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto") internal static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto")
} }
} }
internal enum SavePhotoFailure { internal enum SavePhotoFailure {

View File

@ -0,0 +1,17 @@
//
// MastodonAuthenticationBox.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-20.
//
import Foundation
import MastodonSDK
import CoreDataStack
struct MastodonAuthenticationBox {
let domain: String
let userID: MastodonUser.ID
let appAuthorization: Mastodon.API.OAuth.Authorization
let userAuthorization: Mastodon.API.OAuth.Authorization
}

View File

@ -49,7 +49,7 @@ extension UserProviderFacade {
private static func _toggleUserFollowRelationship( private static func _toggleUserFollowRelationship(
context: AppContext, context: AppContext,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, activeMastodonAuthenticationBox: MastodonAuthenticationBox,
mastodonUser: AnyPublisher<MastodonUser?, Never> mastodonUser: AnyPublisher<MastodonUser?, Never>
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
mastodonUser mastodonUser
@ -111,7 +111,7 @@ extension UserProviderFacade {
private static func _toggleUserBlockRelationship( private static func _toggleUserBlockRelationship(
context: AppContext, context: AppContext,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, activeMastodonAuthenticationBox: MastodonAuthenticationBox,
mastodonUser: AnyPublisher<MastodonUser?, Never> mastodonUser: AnyPublisher<MastodonUser?, Never>
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
mastodonUser mastodonUser
@ -174,7 +174,7 @@ extension UserProviderFacade {
private static func _toggleUserMuteRelationship( private static func _toggleUserMuteRelationship(
context: AppContext, context: AppContext,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, activeMastodonAuthenticationBox: MastodonAuthenticationBox,
mastodonUser: AnyPublisher<MastodonUser?, Never> mastodonUser: AnyPublisher<MastodonUser?, Never>
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
mastodonUser mastodonUser

View File

@ -10,8 +10,8 @@
"Common.Alerts.DiscardPostContent.Title" = "Discard Draft"; "Common.Alerts.DiscardPostContent.Title" = "Discard Draft";
"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again."; "Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again.";
"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error"; "Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error";
"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video.";
"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a post that already contains images."; "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a post that already contains images.";
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
Please check your internet connection."; Please check your internet connection.";
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; "Common.Alerts.PublishPostFailure.Title" = "Publish Failure";

View File

@ -10,8 +10,8 @@
"Common.Alerts.DiscardPostContent.Title" = "Discard Draft"; "Common.Alerts.DiscardPostContent.Title" = "Discard Draft";
"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again."; "Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again.";
"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error"; "Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error";
"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video.";
"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a post that already contains images."; "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a post that already contains images.";
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
Please check your internet connection."; Please check your internet connection.";
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; "Common.Alerts.PublishPostFailure.Title" = "Publish Failure";

View File

@ -28,7 +28,7 @@ final class ComposeViewModel: NSObject {
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false) let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
let selectedStatusVisibility: CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never> let selectedStatusVisibility: CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, Never>
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never> let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never> let activeAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make initial event emit
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero) let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0) let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
@ -202,13 +202,13 @@ final class ComposeViewModel: NSObject {
} }
.assign(to: \.value, on: characterCount) .assign(to: \.value, on: characterCount)
.store(in: &disposeBag) .store(in: &disposeBag)
// bind compose bar button item UI state // bind compose bar button item UI state
let isComposeContentEmpty = composeStatusAttribute.composeContent let isComposeContentEmpty = composeStatusAttribute.composeContent
.map { ($0 ?? "").isEmpty } .map { ($0 ?? "").isEmpty }
let isComposeContentValid = composeStatusAttribute.composeContent let isComposeContentValid = characterCount
.map { composeContent -> Bool in .map { characterCount -> Bool in
let composeContent = composeContent ?? "" return characterCount <= ComposeViewModel.composeContentLimit
return composeContent.count <= ComposeViewModel.composeContentLimit
} }
let isMediaEmpty = attachmentServices let isMediaEmpty = attachmentServices
.map { $0.isEmpty } .map { $0.isEmpty }
@ -224,10 +224,10 @@ final class ComposeViewModel: NSObject {
} }
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
isComposeContentEmpty.eraseToAnyPublisher(), isComposeContentEmpty,
isComposeContentValid.eraseToAnyPublisher(), isComposeContentValid,
isMediaEmpty.eraseToAnyPublisher(), isMediaEmpty,
isMediaUploadAllSuccess.eraseToAnyPublisher() isMediaUploadAllSuccess
) )
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
if isMediaEmpty { if isMediaEmpty {
@ -239,10 +239,10 @@ final class ComposeViewModel: NSObject {
.eraseToAnyPublisher() .eraseToAnyPublisher()
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
isComposeContentEmpty.eraseToAnyPublisher(), isComposeContentEmpty,
isComposeContentValid.eraseToAnyPublisher(), isComposeContentValid,
isPollComposing.eraseToAnyPublisher(), isPollComposing,
isPollAttributeAllValid.eraseToAnyPublisher() isPollAttributeAllValid
) )
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
if isPollComposing { if isPollComposing {
@ -390,9 +390,9 @@ extension ComposeViewModel {
var failureReason: String? { var failureReason: String? {
switch self { switch self {
case .videoAttachWithPhoto: case .videoAttachWithPhoto:
return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.videoAttachWithPhoto return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
case .moreThanOneVideo: case .moreThanOneVideo:
return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.moreThanOneVideo return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
} }
} }
} }

View File

@ -116,9 +116,11 @@ extension ComposeStatusAttachmentTableViewCell {
} else { } else {
guard let uploadState = uploadState else { return } guard let uploadState = uploadState else { return }
switch uploadState { switch uploadState {
case is MastodonAttachmentService.UploadState.Finish, case is MastodonAttachmentService.UploadState.Finish:
is MastodonAttachmentService.UploadState.Fail:
cell.attachmentContainerView.activityIndicatorView.stopAnimating() cell.attachmentContainerView.activityIndicatorView.stopAnimating()
case is MastodonAttachmentService.UploadState.Fail:
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
// FIXME: not display
cell.attachmentContainerView.emptyStateView.label.text = { cell.attachmentContainerView.emptyStateView.label.text = {
if let file = attachmentService.file.value { if let file = attachmentService.file.value {
switch file { switch file {

View File

@ -26,7 +26,7 @@ final class NotificationViewModel: NSObject {
let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.EveryThing) let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.EveryThing)
let noMoreNotification = CurrentValueSubject<Bool, Never>(false) let noMoreNotification = CurrentValueSubject<Bool, Never>(false)
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never> let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>! let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil) let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
let cellFrameCache = NSCache<NSString, NSValue>() let cellFrameCache = NSCache<NSString, NSValue>()

View File

@ -17,7 +17,7 @@ final class FavoriteViewModel {
// input // input
let context: AppContext let context: AppContext
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never> let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let cellFrameCache = NSCache<NSNumber, NSValue>() let cellFrameCache = NSCache<NSNumber, NSValue>()

View File

@ -115,7 +115,7 @@ class ProfileViewModel: NSObject {
context.authenticationService.activeMastodonAuthenticationBox.eraseToAnyPublisher(), context.authenticationService.activeMastodonAuthenticationBox.eraseToAnyPublisher(),
pendingRetryPublisher.eraseToAnyPublisher() pendingRetryPublisher.eraseToAnyPublisher()
) )
.compactMap { mastodonUserID, activeMastodonAuthenticationBox, _ -> (String, AuthenticationService.MastodonAuthenticationBox)? in .compactMap { mastodonUserID, activeMastodonAuthenticationBox, _ -> (String, MastodonAuthenticationBox)? in
guard let mastodonUserID = mastodonUserID, let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return nil } guard let mastodonUserID = mastodonUserID, let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return nil }
guard mastodonUserID != activeMastodonAuthenticationBox.userID else { return nil } guard mastodonUserID != activeMastodonAuthenticationBox.userID else { return nil }
return (mastodonUserID, activeMastodonAuthenticationBox) return (mastodonUserID, activeMastodonAuthenticationBox)

View File

@ -17,7 +17,7 @@ extension ReportViewModel {
func requestRecentStatus( func requestRecentStatus(
domain: String, domain: String,
accountId: String, accountId: String,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) { ) {
context.apiService.userTimeline( context.apiService.userTimeline(
domain: domain, domain: domain,

View File

@ -165,7 +165,7 @@ class ReportViewModel: NSObject {
.store(in: &disposeBag) .store(in: &disposeBag)
} }
func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> AnyPublisher<(Bool, Error?), Never> { func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: MastodonAuthenticationBox) -> AnyPublisher<(Bool, Error?), Never> {
let skip = input.step2Skip.map { [weak self] value -> Void in let skip = input.step2Skip.map { [weak self] value -> Void in
guard let self = self else { return value } guard let self = self else { return value }
self.reportQuery.comment = nil self.reportQuery.comment = nil

View File

@ -42,7 +42,7 @@ final class SearchViewModel: NSObject {
context.authenticationService.activeMastodonAuthenticationBox, context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared viewDidAppeared
) )
.compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in .compactMap { activeMastodonAuthenticationBox, _ -> MastodonAuthenticationBox? in
return activeMastodonAuthenticationBox return activeMastodonAuthenticationBox
} }
.throttle(for: 1, scheduler: DispatchQueue.main, latest: false) .throttle(for: 1, scheduler: DispatchQueue.main, latest: false)
@ -72,7 +72,7 @@ final class SearchViewModel: NSObject {
context.authenticationService.activeMastodonAuthenticationBox, context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared viewDidAppeared
) )
.compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in .compactMap { activeMastodonAuthenticationBox, _ -> MastodonAuthenticationBox? in
return activeMastodonAuthenticationBox return activeMastodonAuthenticationBox
} }
.throttle(for: 1, scheduler: DispatchQueue.main, latest: false) .throttle(for: 1, scheduler: DispatchQueue.main, latest: false)

View File

@ -16,7 +16,7 @@ extension APIService {
func toggleBlock( func toggleBlock(
for mastodonUser: MastodonUser, for mastodonUser: MastodonUser,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox activeMastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
let notificationFeedbackGenerator = UINotificationFeedbackGenerator() let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
@ -86,7 +86,7 @@ extension APIService {
// update database local and return block query update type for remote request // update database local and return block query update type for remote request
func blockUpdateLocal( func blockUpdateLocal(
mastodonUserObjectID: NSManagedObjectID, mastodonUserObjectID: NSManagedObjectID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<(Mastodon.API.Account.BlockQueryType, MastodonUser.ID), Error> { ) -> AnyPublisher<(Mastodon.API.Account.BlockQueryType, MastodonUser.ID), Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID
@ -132,7 +132,7 @@ extension APIService {
func blockUpdateRemote( func blockUpdateRemote(
blockQueryType: Mastodon.API.Account.BlockQueryType, blockQueryType: Mastodon.API.Account.BlockQueryType,
mastodonUserID: MastodonUser.ID, mastodonUserID: MastodonUser.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -17,7 +17,7 @@ extension APIService {
func getDomainblocks( func getDomainblocks(
domain: String, domain: String,
limit: Int = onceRequestDomainBlocksMaxCount, limit: Int = onceRequestDomainBlocksMaxCount,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[String]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[String]>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
@ -71,7 +71,7 @@ extension APIService {
func blockDomain( func blockDomain(
user: MastodonUser, user: MastodonUser,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
@ -105,7 +105,7 @@ extension APIService {
func unblockDomain( func unblockDomain(
user: MastodonUser, user: MastodonUser,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization

View File

@ -61,7 +61,7 @@ extension APIService {
func favorite( func favorite(
statusID: Mastodon.Entity.Status.ID, statusID: Mastodon.Entity.Status.ID,
favoriteKind: Mastodon.API.Favorites.FavoriteKind, favoriteKind: Mastodon.API.Favorites.FavoriteKind,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID
@ -139,7 +139,7 @@ extension APIService {
func favoritedStatuses( func favoritedStatuses(
limit: Int = onceRequestStatusMaxCount, limit: Int = onceRequestStatusMaxCount,
maxID: String? = nil, maxID: String? = nil,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID

View File

@ -15,7 +15,7 @@ import MastodonSDK
extension APIService { extension APIService {
func filters( func filters(
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain

View File

@ -24,7 +24,7 @@ extension APIService {
/// - Returns: publisher for `Relationship` /// - Returns: publisher for `Relationship`
func toggleFollow( func toggleFollow(
for mastodonUser: MastodonUser, for mastodonUser: MastodonUser,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox activeMastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
@ -96,7 +96,7 @@ extension APIService {
// update database local and return follow query update type for remote request // update database local and return follow query update type for remote request
func followUpdateLocal( func followUpdateLocal(
mastodonUserObjectID: NSManagedObjectID, mastodonUserObjectID: NSManagedObjectID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<(Mastodon.API.Account.FollowQueryType, MastodonUser.ID), Error> { ) -> AnyPublisher<(Mastodon.API.Account.FollowQueryType, MastodonUser.ID), Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID
@ -156,7 +156,7 @@ extension APIService {
func followUpdateRemote( func followUpdateRemote(
followQueryType: Mastodon.API.Account.FollowQueryType, followQueryType: Mastodon.API.Account.FollowQueryType,
mastodonUserID: MastodonUser.ID, mastodonUserID: MastodonUser.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -17,7 +17,7 @@ import MastodonSDK
extension APIService { extension APIService {
func acceptFollowRequest( func acceptFollowRequest(
mastodonUserID: MastodonUser.ID, mastodonUserID: MastodonUser.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
@ -61,7 +61,7 @@ extension APIService {
func rejectFollowRequest( func rejectFollowRequest(
mastodonUserID: MastodonUser.ID, mastodonUserID: MastodonUser.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -22,7 +22,7 @@ extension APIService {
limit: Int = onceRequestStatusMaxCount, limit: Int = onceRequestStatusMaxCount,
local: Bool? = nil, local: Bool? = nil,
hashtag: String, hashtag: String,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
let requestMastodonUserID = authorizationBox.userID let requestMastodonUserID = authorizationBox.userID

View File

@ -21,7 +21,7 @@ extension APIService {
maxID: Mastodon.Entity.Status.ID? = nil, maxID: Mastodon.Entity.Status.ID? = nil,
limit: Int = onceRequestStatusMaxCount, limit: Int = onceRequestStatusMaxCount,
local: Bool? = nil, local: Bool? = nil,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
let requestMastodonUserID = authorizationBox.userID let requestMastodonUserID = authorizationBox.userID

View File

@ -14,7 +14,7 @@ extension APIService {
func uploadMedia( func uploadMedia(
domain: String, domain: String,
query: Mastodon.API.Media.UploadMediaQuery, query: Mastodon.API.Media.UploadMediaQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, mastodonAuthenticationBox: MastodonAuthenticationBox,
needsFallback: Bool needsFallback: Bool
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
if needsFallback { if needsFallback {
@ -27,7 +27,7 @@ extension APIService {
private func uploadMediaV1( private func uploadMediaV1(
domain: String, domain: String,
query: Mastodon.API.Media.UploadMediaQuery, query: Mastodon.API.Media.UploadMediaQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
@ -42,7 +42,7 @@ extension APIService {
private func uploadMediaV2( private func uploadMediaV2(
domain: String, domain: String,
query: Mastodon.API.Media.UploadMediaQuery, query: Mastodon.API.Media.UploadMediaQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
@ -54,12 +54,16 @@ extension APIService {
) )
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
}
extension APIService {
func updateMedia( func updateMedia(
domain: String, domain: String,
attachmentID: Mastodon.Entity.Attachment.ID, attachmentID: Mastodon.Entity.Attachment.ID,
query: Mastodon.API.Media.UpdateMediaQuery, query: Mastodon.API.Media.UpdateMediaQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -16,7 +16,7 @@ extension APIService {
func toggleMute( func toggleMute(
for mastodonUser: MastodonUser, for mastodonUser: MastodonUser,
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox activeMastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
let notificationFeedbackGenerator = UINotificationFeedbackGenerator() let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
@ -86,7 +86,7 @@ extension APIService {
// update database local and return mute query update type for remote request // update database local and return mute query update type for remote request
func muteUpdateLocal( func muteUpdateLocal(
mastodonUserObjectID: NSManagedObjectID, mastodonUserObjectID: NSManagedObjectID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<(Mastodon.API.Account.MuteQueryType, MastodonUser.ID), Error> { ) -> AnyPublisher<(Mastodon.API.Account.MuteQueryType, MastodonUser.ID), Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID
@ -132,7 +132,7 @@ extension APIService {
func muteUpdateRemote( func muteUpdateRemote(
muteQueryType: Mastodon.API.Account.MuteQueryType, muteQueryType: Mastodon.API.Account.MuteQueryType,
mastodonUserID: MastodonUser.ID, mastodonUserID: MastodonUser.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -16,7 +16,7 @@ extension APIService {
func allNotifications( func allNotifications(
domain: String, domain: String,
query: Mastodon.API.Notifications.Query, query: Mastodon.API.Notifications.Query,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let userID = mastodonAuthenticationBox.userID let userID = mastodonAuthenticationBox.userID
@ -75,7 +75,7 @@ extension APIService {
func notification( func notification(
notificationID: Mastodon.Entity.Notification.ID, notificationID: Mastodon.Entity.Notification.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -19,7 +19,7 @@ extension APIService {
domain: String, domain: String,
pollID: Mastodon.Entity.Poll.ID, pollID: Mastodon.Entity.Poll.ID,
pollObjectID: NSManagedObjectID, pollObjectID: NSManagedObjectID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID
@ -143,7 +143,7 @@ extension APIService {
pollID: Mastodon.Entity.Poll.ID, pollID: Mastodon.Entity.Poll.ID,
pollObjectID: NSManagedObjectID, pollObjectID: NSManagedObjectID,
choices: [Int], choices: [Int],
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID

View File

@ -62,7 +62,7 @@ extension APIService {
func reblog( func reblog(
statusID: Mastodon.Entity.Status.ID, statusID: Mastodon.Entity.Status.ID,
reblogKind: Mastodon.API.Reblog.ReblogKind, reblogKind: Mastodon.API.Reblog.ReblogKind,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -16,7 +16,7 @@ extension APIService {
func suggestionAccount( func suggestionAccount(
domain: String, domain: String,
query: Mastodon.API.Suggestions.Query?, query: Mastodon.API.Suggestions.Query?,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
@ -47,7 +47,7 @@ extension APIService {
func suggestionAccountV2( func suggestionAccountV2(
domain: String, domain: String,
query: Mastodon.API.Suggestions.Query?, query: Mastodon.API.Suggestions.Query?,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -17,7 +17,7 @@ extension APIService {
func relationship( func relationship(
domain: String, domain: String,
accountIDs: [Mastodon.Entity.Account.ID], accountIDs: [Mastodon.Entity.Account.ID],
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
let requestMastodonUserID = authorizationBox.userID let requestMastodonUserID = authorizationBox.userID

View File

@ -14,7 +14,7 @@ extension APIService {
func report( func report(
domain: String, domain: String,
query: Mastodon.API.Reports.FileReportQuery, query: Mastodon.API.Reports.FileReportQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Bool>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Bool>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization

View File

@ -15,7 +15,7 @@ extension APIService {
func search( func search(
domain: String, domain: String,
query: Mastodon.API.V2.Search.Query, query: Mastodon.API.V2.Search.Query,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let requestMastodonUserID = mastodonAuthenticationBox.userID let requestMastodonUserID = mastodonAuthenticationBox.userID

View File

@ -0,0 +1,60 @@
//
// APIService+Status+Publish.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-20.
//
import Foundation
import Combine
import CoreData
import CoreDataStack
import CommonOSLog
import MastodonSDK
extension APIService {
func publishStatus(
domain: String,
query: Mastodon.API.Statuses.PublishStatusQuery,
mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization
return Mastodon.API.Statuses.publishStatus(
session: session,
domain: domain,
query: query,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
#if APP_EXTENSION
return Just(response)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
#else
return APIService.Persist.persistStatus(
managedObjectContext: self.backgroundManagedObjectContext,
domain: domain,
query: nil,
response: response.map { [$0] },
persistType: .lookUp,
requestMastodonUserID: nil,
log: OSLog.api
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
#endif
}
.eraseToAnyPublisher()
}
}

View File

@ -14,48 +14,11 @@ import DateToolsSwift
import MastodonSDK import MastodonSDK
extension APIService { extension APIService {
func publishStatus(
domain: String,
query: Mastodon.API.Statuses.PublishStatusQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization
return Mastodon.API.Statuses.publishStatus(
session: session,
domain: domain,
query: query,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
return APIService.Persist.persistStatus(
managedObjectContext: self.backgroundManagedObjectContext,
domain: domain,
query: nil,
response: response.map { [$0] },
persistType: .lookUp,
requestMastodonUserID: nil,
log: OSLog.api
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
func status( func status(
domain: String, domain: String,
statusID: Mastodon.Entity.Status.ID, statusID: Mastodon.Entity.Status.ID,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
return Mastodon.API.Statuses.status( return Mastodon.API.Statuses.status(
@ -91,7 +54,7 @@ extension APIService {
func deleteStatus( func deleteStatus(
domain: String, domain: String,
statusID: Mastodon.Entity.Status.ID, statusID: Mastodon.Entity.Status.ID,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
let query = Mastodon.API.Statuses.DeleteStatusQuery(id: statusID) let query = Mastodon.API.Statuses.DeleteStatusQuery(id: statusID)

View File

@ -17,7 +17,7 @@ extension APIService {
func createSubscription( func createSubscription(
subscriptionObjectID: NSManagedObjectID, subscriptionObjectID: NSManagedObjectID,
query: Mastodon.API.Subscriptions.CreateSubscriptionQuery, query: Mastodon.API.Subscriptions.CreateSubscriptionQuery,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain
@ -50,7 +50,7 @@ extension APIService {
} }
func cancelSubscription( func cancelSubscription(
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.EmptySubscription>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.EmptySubscription>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
let domain = mastodonAuthenticationBox.domain let domain = mastodonAuthenticationBox.domain

View File

@ -17,7 +17,7 @@ extension APIService {
func statusContext( func statusContext(
domain: String, domain: String,
statusID: Mastodon.Entity.Status.ID, statusID: Mastodon.Entity.Status.ID,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Context>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Context>, Error> {
let authorization = mastodonAuthenticationBox.userAuthorization let authorization = mastodonAuthenticationBox.userAuthorization
guard domain == mastodonAuthenticationBox.domain else { guard domain == mastodonAuthenticationBox.domain else {

View File

@ -23,7 +23,7 @@ extension APIService {
excludeReplies: Bool? = nil, excludeReplies: Bool? = nil,
excludeReblogs: Bool? = nil, excludeReblogs: Bool? = nil,
onlyMedia: Bool? = nil, onlyMedia: Bool? = nil,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> { ) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
let authorization = authorizationBox.userAuthorization let authorization = authorizationBox.userAuthorization
let requestMastodonUserID = authorizationBox.userID let requestMastodonUserID = authorizationBox.userID

View File

@ -21,7 +21,6 @@ final class APIService {
// internal // internal
let session: URLSession let session: URLSession
// input // input
let backgroundManagedObjectContext: NSManagedObjectContext let backgroundManagedObjectContext: NSManagedObjectContext

View File

@ -24,9 +24,9 @@ final class AuthenticationService: NSObject {
// output // output
let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([]) let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([])
let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([]) let mastodonAuthenticationBoxes = CurrentValueSubject<[MastodonAuthenticationBox], Never>([])
let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil) let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(nil) let activeMastodonAuthenticationBox = CurrentValueSubject<MastodonAuthenticationBox?, Never>(nil)
init( init(
managedObjectContext: NSManagedObjectContext, managedObjectContext: NSManagedObjectContext,
@ -61,11 +61,11 @@ final class AuthenticationService: NSObject {
.store(in: &disposeBag) .store(in: &disposeBag)
mastodonAuthentications mastodonAuthentications
.map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in .map { authentications -> [MastodonAuthenticationBox] in
return authentications return authentications
.sorted(by: { $0.activedAt > $1.activedAt }) .sorted(by: { $0.activedAt > $1.activedAt })
.compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in .compactMap { authentication -> MastodonAuthenticationBox? in
return AuthenticationService.MastodonAuthenticationBox( return MastodonAuthenticationBox(
domain: authentication.domain, domain: authentication.domain,
userID: authentication.userID, userID: authentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
@ -91,15 +91,6 @@ final class AuthenticationService: NSObject {
} }
extension AuthenticationService {
struct MastodonAuthenticationBox {
let domain: String
let userID: MastodonUser.ID
let appAuthorization: Mastodon.API.OAuth.Authorization
let userAuthorization: Mastodon.API.OAuth.Authorization
}
}
extension AuthenticationService { extension AuthenticationService {
func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> { func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
@ -133,7 +124,7 @@ extension AuthenticationService {
guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else { guard let mastodonAuthentication = try? managedObjectContext.fetch(request).first else {
return return
} }
_mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox( _mastodonAuthenticationBox = MastodonAuthenticationBox(
domain: mastodonAuthentication.domain, domain: mastodonAuthentication.domain,
userID: mastodonAuthentication.userID, userID: mastodonAuthentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken), appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken),

View File

@ -83,7 +83,7 @@ extension MastodonAttachmentService.UploadState {
{ {
self.needsFallback = true self.needsFallback = true
stateMachine.enter(Uploading.self) stateMachine.enter(Uploading.self)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment fallback to V1", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment fallback to V1", ((#file as NSString).lastPathComponent), #line, #function)
} else { } else {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
service.error.send(error) service.error.send(error)

View File

@ -27,7 +27,7 @@ final class MastodonAttachmentService {
// input // input
let context: AppContext let context: AppContext
var authenticationBox: AuthenticationService.MastodonAuthenticationBox? var authenticationBox: MastodonAuthenticationBox?
let file = CurrentValueSubject<Mastodon.Query.MediaAttachment?, Never>(nil) let file = CurrentValueSubject<Mastodon.Query.MediaAttachment?, Never>(nil)
let description = CurrentValueSubject<String?, Never>(nil) let description = CurrentValueSubject<String?, Never>(nil)
@ -52,7 +52,7 @@ final class MastodonAttachmentService {
init( init(
context: AppContext, context: AppContext,
pickerResult: PHPickerResult, pickerResult: PHPickerResult,
initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? initialAuthenticationBox: MastodonAuthenticationBox?
) { ) {
self.context = context self.context = context
self.authenticationBox = initialAuthenticationBox self.authenticationBox = initialAuthenticationBox
@ -90,7 +90,7 @@ final class MastodonAttachmentService {
init( init(
context: AppContext, context: AppContext,
image: UIImage, image: UIImage,
initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? initialAuthenticationBox: MastodonAuthenticationBox?
) { ) {
self.context = context self.context = context
self.authenticationBox = initialAuthenticationBox self.authenticationBox = initialAuthenticationBox
@ -105,7 +105,7 @@ final class MastodonAttachmentService {
init( init(
context: AppContext, context: AppContext,
documentURL: URL, documentURL: URL,
initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? initialAuthenticationBox: MastodonAuthenticationBox?
) { ) {
self.context = context self.context = context
self.authenticationBox = initialAuthenticationBox self.authenticationBox = initialAuthenticationBox
@ -191,7 +191,7 @@ extension MastodonAttachmentService {
extension MastodonAttachmentService { extension MastodonAttachmentService {
// FIXME: needs reset state for multiple account posting support // FIXME: needs reset state for multiple account posting support
func uploading(mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> Bool { func uploading(mastodonAuthenticationBox: MastodonAuthenticationBox) -> Bool {
authenticationBox = mastodonAuthenticationBox authenticationBox = mastodonAuthenticationBox
return uploadStateMachine.enter(UploadState.self) return uploadStateMachine.enter(UploadState.self)
} }

View File

@ -82,7 +82,7 @@ extension NotificationService {
extension NotificationService { extension NotificationService {
func dequeueNotificationViewModel( func dequeueNotificationViewModel(
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> NotificationViewModel? { ) -> NotificationViewModel? {
var _notificationSubscription: NotificationViewModel? var _notificationSubscription: NotificationViewModel?
workingQueue.sync { workingQueue.sync {
@ -130,7 +130,7 @@ extension NotificationService {
// cancel subscription if sign-out // cancel subscription if sign-out
let accessToken = mastodonPushNotification.accessToken let accessToken = mastodonPushNotification.accessToken
let mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox( let mastodonAuthenticationBox = MastodonAuthenticationBox(
domain: domain, domain: domain,
userID: userID, userID: userID,
appAuthorization: .init(accessToken: accessToken), appAuthorization: .init(accessToken: accessToken),
@ -178,7 +178,7 @@ extension NotificationService.NotificationViewModel {
func createSubscribeQuery( func createSubscribeQuery(
deviceToken: Data, deviceToken: Data,
queryData: Mastodon.API.Subscriptions.QueryData, queryData: Mastodon.API.Subscriptions.QueryData,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox mastodonAuthenticationBox: MastodonAuthenticationBox
) -> Mastodon.API.Subscriptions.CreateSubscriptionQuery { ) -> Mastodon.API.Subscriptions.CreateSubscriptionQuery {
let deviceToken = [UInt8](deviceToken).toHexString() let deviceToken = [UInt8](deviceToken).toHexString()

View File

@ -41,7 +41,7 @@ final class SettingService {
// create setting (if non-exist) for authenticated users // create setting (if non-exist) for authenticated users
authenticationService.mastodonAuthenticationBoxes authenticationService.mastodonAuthenticationBoxes
.compactMap { [weak self] mastodonAuthenticationBoxes -> AnyPublisher<[AuthenticationService.MastodonAuthenticationBox], Never>? in .compactMap { [weak self] mastodonAuthenticationBoxes -> AnyPublisher<[MastodonAuthenticationBox], Never>? in
guard let self = self else { return nil } guard let self = self else { return nil }
guard let authenticationService = self.authenticationService else { return nil } guard let authenticationService = self.authenticationService else { return nil }
guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return nil } guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return nil }

View File

@ -104,7 +104,7 @@ extension StatusPrefetchingService {
statusObjectID: NSManagedObjectID, statusObjectID: NSManagedObjectID,
statusID: Mastodon.Entity.Status.ID, statusID: Mastodon.Entity.Status.ID,
replyToStatusID: Mastodon.Entity.Status.ID, replyToStatusID: Mastodon.Entity.Status.ID,
authorizationBox: AuthenticationService.MastodonAuthenticationBox authorizationBox: MastodonAuthenticationBox
) { ) {
workingQueue.async { [weak self] in workingQueue.async { [weak self] in
guard let self = self, let apiService = self.apiService else { return } guard let self = self, let apiService = self.apiService else { return }

View File

@ -0,0 +1,42 @@
//
// CGImage.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-31.
//
import CoreImage
extension CGImage {
// Reference
// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
// Luma Y = 0.2126R + 0.7152G + 0.0722B
public var brightness: CGFloat? {
let context = CIContext() // default with metal accelerate
let ciImage = CIImage(cgImage: self)
let rec709Image = context.createCGImage(
ciImage,
from: ciImage.extent,
format: .RGBA8,
colorSpace: CGColorSpace(name: CGColorSpace.itur_709) // BT.709 a.k.a Rec.709
)
guard let image = rec709Image,
image.bitsPerPixel == 32,
let data = rec709Image?.dataProvider?.data,
let pointer = CFDataGetBytePtr(data) else { return nil }
let length = CFDataGetLength(data)
guard length > 0 else { return nil }
var luma: CGFloat = 0.0
for i in stride(from: 0, to: length, by: 4) {
let r = pointer[i]
let g = pointer[i + 1]
let b = pointer[i + 2]
let Y = 0.2126 * CGFloat(r) + 0.7152 * CGFloat(g) + 0.0722 * CGFloat(b)
luma += Y
}
luma /= CGFloat(width * height)
return luma
}
}

View File

@ -1,10 +1,12 @@
// //
// UIImage.swift // UIImage.swift
// // Mastodon
// //
// Created by MainasuK Cirno on 2021-7-16. // Created by sxiaojian on 2021/3/8.
// //
import CoreImage
import CoreImage.CIFilterBuiltins
import UIKit import UIKit
extension UIImage { extension UIImage {
@ -17,3 +19,74 @@ extension UIImage {
} }
} }
} }
// refs: https://www.hackingwithswift.com/example-code/media/how-to-read-the-average-color-of-a-uiimage-using-ciareaaverage
extension UIImage {
@available(iOS 14.0, *)
public var dominantColor: UIColor? {
guard let inputImage = CIImage(image: self) else { return nil }
let filter = CIFilter.areaAverage()
filter.inputImage = inputImage
filter.extent = inputImage.extent
guard let outputImage = filter.outputImage else { return nil }
var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}
extension UIImage {
public var domainLumaCoefficientsStyle: UIUserInterfaceStyle? {
guard let brightness = cgImage?.brightness else { return nil }
return brightness > 100 ? .light : .dark // 0 ~ 255
}
}
extension UIImage {
public func blur(radius: CGFloat) -> UIImage? {
guard let inputImage = CIImage(image: self) else { return nil }
let blurFilter = CIFilter.gaussianBlur()
blurFilter.inputImage = inputImage
blurFilter.radius = Float(radius)
guard let outputImage = blurFilter.outputImage else { return nil }
guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { return nil }
let image = UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation)
return image
}
}
extension UIImage {
public func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? {
let maxRadius = min(size.width, size.height) / 2
let cornerRadius: CGFloat = {
guard let radius = radius, radius > 0 else { return maxRadius }
return min(radius, maxRadius)
}()
let render = UIGraphicsImageRenderer(size: size)
return render.image { (_: UIGraphicsImageRendererContext) in
let rect = CGRect(origin: .zero, size: size)
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip()
draw(in: rect)
}
}
}
extension UIImage {
public static func adaptiveUserInterfaceStyleImage(lightImage: UIImage, darkImage: UIImage) -> UIImage {
let imageAsset = UIImageAsset()
imageAsset.register(lightImage, with: UITraitCollection(traitsFrom: [
UITraitCollection(displayScale: 1.0),
UITraitCollection(userInterfaceStyle: .light)
]))
imageAsset.register(darkImage, with: UITraitCollection(traitsFrom: [
UITraitCollection(displayScale: 1.0),
UITraitCollection(userInterfaceStyle: .dark)
]))
return imageAsset.image(with: UITraitCollection.current)
}
}

View File

@ -41,11 +41,17 @@ extension ItemProviderLoader {
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else { guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
return return
} }
#if APP_EXTENSION
let maxPixelSize: Int = 4096 // not limit but may upload fail
#else
let maxPixelSize: Int = 1536 // fit 120MB RAM limit
#endif
let downsampleOptions = [ let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: 4096, kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
] as CFDictionary ] as CFDictionary
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {

View File

@ -34,8 +34,7 @@ class ShareViewController: UIViewController {
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ShareViewController.cancelBarButtonItemPressed(_:))) private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ShareViewController.cancelBarButtonItemPressed(_:)))
private(set) lazy var publishBarButtonItem: UIBarButtonItem = { private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(customView: publishButton) let barButtonItem = UIBarButtonItem(customView: publishButton)
barButtonItem.target = self publishButton.addTarget(self, action: #selector(ShareViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
barButtonItem.action = #selector(ShareViewController.publishBarButtonItemPressed(_:))
return barButtonItem return barButtonItem
}() }()
@ -173,6 +172,12 @@ extension ShareViewController {
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// bind valid
viewModel.isValid
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: publishButton)
.store(in: &disposeBag)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -180,6 +185,8 @@ extension ShareViewController {
viewModel.viewDidAppear.value = true viewModel.viewDidAppear.value = true
viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
viewModel.composeViewModel.viewDidAppear = true
} }
override func viewSafeAreaInsetsDidChange() { override func viewSafeAreaInsetsDidChange() {
@ -204,6 +211,42 @@ extension ShareViewController {
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
viewModel.isPublishing.value = true
viewModel.publish()
.delay(for: 2, scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
self.viewModel.isPublishing.value = false
switch completion {
case .failure:
let alertController = UIAlertController(
title: L10n.Common.Alerts.PublishPostFailure.title,
message: L10n.Common.Alerts.PublishPostFailure.message,
preferredStyle: .actionSheet // can not use alert in extension
)
let okAction = UIAlertAction(
title: L10n.Common.Controls.Actions.ok,
style: .cancel,
handler: nil
)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
case .finished:
self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal)
self.publishButton.isUserInteractionEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
guard let self = self else { return }
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
} receiveValue: { response in
// do nothing
}
.store(in: &disposeBag)
} }
} }

View File

@ -10,6 +10,7 @@ import Foundation
import Combine import Combine
import CoreData import CoreData
import CoreDataStack import CoreDataStack
import MastodonSDK
import MastodonUI import MastodonUI
import SwiftUI import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
@ -33,6 +34,7 @@ final class ShareViewModel {
// output // output
let authentication = CurrentValueSubject<Result<MastodonAuthentication, Error>?, Never>(nil) let authentication = CurrentValueSubject<Result<MastodonAuthentication, Error>?, Never>(nil)
let isFetchAuthentication = CurrentValueSubject<Bool, Never>(true) let isFetchAuthentication = CurrentValueSubject<Bool, Never>(true)
let isPublishing = CurrentValueSubject<Bool, Never>(false)
let isBusy = CurrentValueSubject<Bool, Never>(true) let isBusy = CurrentValueSubject<Bool, Never>(true)
let isValid = CurrentValueSubject<Bool, Never>(false) let isValid = CurrentValueSubject<Bool, Never>(false)
let composeViewModel = ComposeViewModel() let composeViewModel = ComposeViewModel()
@ -59,11 +61,13 @@ final class ShareViewModel {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// bind authentication loading state
authentication authentication
.map { result in result == nil } .map { result in result == nil }
.assign(to: \.value, on: isFetchAuthentication) .assign(to: \.value, on: isFetchAuthentication)
.store(in: &disposeBag) .store(in: &disposeBag)
// bind user locked state
authentication authentication
.compactMap { result -> Bool? in .compactMap { result -> Bool? in
guard let result = result else { return nil } guard let result = result else { return nil }
@ -80,15 +84,105 @@ final class ShareViewModel {
.assign(to: \.value, on: selectedStatusVisibility) .assign(to: \.value, on: selectedStatusVisibility)
.store(in: &disposeBag) .store(in: &disposeBag)
isFetchAuthentication // bind author
authentication
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.assign(to: \.value, on: isBusy) .sink { [weak self] result in
guard let self = self else { return }
guard let result = result else { return }
switch result {
case .success(let authentication):
self.composeViewModel.avatarImageURL = authentication.user.avatarImageURL()
self.composeViewModel.authorName = authentication.user.displayNameWithFallback
self.composeViewModel.authorUsername = "@" + authentication.user.username
case .failure:
self.composeViewModel.avatarImageURL = nil
self.composeViewModel.authorName = " "
self.composeViewModel.authorUsername = " "
}
}
.store(in: &disposeBag) .store(in: &disposeBag)
// bind authentication to compose view model
authentication
.map { result -> MastodonAuthentication? in
guard let result = result else { return nil }
switch result {
case .success(let authentication):
return authentication
case .failure:
return nil
}
}
.assign(to: &composeViewModel.$authentication)
// bind isBusy
Publishers.CombineLatest(
isFetchAuthentication,
isPublishing
)
.receive(on: DispatchQueue.main)
.map { $0 || $1 }
.assign(to: \.value, on: isBusy)
.store(in: &disposeBag)
// pass initial i18n string
composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder
composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder
composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight
// bind compose bar button item UI state
let isComposeContentEmpty = composeViewModel.$statusContent
.map { $0.isEmpty }
let isComposeContentValid = composeViewModel.$characterCount
.map { characterCount -> Bool in
return characterCount <= ShareViewModel.composeContentLimit
}
let isMediaEmpty = composeViewModel.$attachmentViewModels
.map { $0.isEmpty }
let isMediaUploadAllSuccess = composeViewModel.$attachmentViewModels
.map { viewModels in
viewModels.allSatisfy { $0.uploadStateMachineSubject.value is StatusAttachmentViewModel.UploadState.Finish }
}
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
isComposeContentEmpty,
isComposeContentValid,
isMediaEmpty,
isMediaUploadAllSuccess
)
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
if isMediaEmpty {
return isComposeContentValid && !isComposeContentEmpty
} else {
return isComposeContentValid && isMediaUploadAllSuccess
}
}
.eraseToAnyPublisher()
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest(
isComposeContentEmpty,
isComposeContentValid
)
.map { isComposeContentEmpty, isComposeContentValid -> Bool in
return isComposeContentValid && !isComposeContentEmpty
}
.eraseToAnyPublisher()
Publishers.CombineLatest(
isPublishBarButtonItemEnabledPrecondition1,
isPublishBarButtonItemEnabledPrecondition2
)
.map { $0 && $1 }
.assign(to: \.value, on: isValid)
.store(in: &disposeBag)
// bind counter
composeViewModel.$characterCount
.assign(to: \.value, on: characterCount)
.store(in: &disposeBag)
// setup theme
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -97,10 +191,6 @@ final class ShareViewModel {
self.setupBackgroundColor(theme: theme) self.setupBackgroundColor(theme: theme)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
composeViewModel.$characterCount
.assign(to: \.value, on: characterCount)
.store(in: &disposeBag)
} }
private func setupBackgroundColor(theme: Theme) { private func setupBackgroundColor(theme: Theme) {
@ -184,3 +274,76 @@ extension ShareViewModel {
} }
} }
} }
extension ShareViewModel {
func publish() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
guard let authentication = composeViewModel.authentication else {
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
}
let mastodonAuthenticationBox = MastodonAuthenticationBox(
domain: authentication.domain,
userID: authentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
)
let domain = authentication.domain
let attachmentViewModels = composeViewModel.attachmentViewModels
let mediaIDs = attachmentViewModels.compactMap { viewModel in
viewModel.attachment.value?.id
}
let sensitive: Bool = composeViewModel.isContentWarningComposing
let spoilerText: String? = {
let text = composeViewModel.contentWarningContent
guard !text.isEmpty else { return nil }
return text
}()
let visibility = selectedStatusVisibility.value.visibility
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
for attachmentViewModel in attachmentViewModels {
guard let attachmentID = attachmentViewModel.attachment.value?.id else { continue }
let description = attachmentViewModel.descriptionContent.trimmingCharacters(in: .whitespacesAndNewlines)
guard !description.isEmpty else { continue }
let query = Mastodon.API.Media.UpdateMediaQuery(
file: nil,
thumbnail: nil,
description: description,
focus: nil
)
let subscription = APIService.shared.updateMedia(
domain: domain,
attachmentID: attachmentID,
query: query,
mastodonAuthenticationBox: mastodonAuthenticationBox
)
subscriptions.append(subscription)
}
return subscriptions
}()
let status = composeViewModel.statusContent
return Publishers.MergeMany(updateMediaQuerySubscriptions)
.collect()
.flatMap { attachments -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
let query = Mastodon.API.Statuses.PublishStatusQuery(
status: status,
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
pollOptions: nil,
pollExpiresIn: nil,
inReplyToID: nil,
sensitive: sensitive,
spoilerText: spoilerText,
visibility: visibility
)
return APIService.shared.publishStatus(
domain: domain,
query: query,
mastodonAuthenticationBox: mastodonAuthenticationBox
)
}
.eraseToAnyPublisher()
}
}

View File

@ -47,7 +47,8 @@ public struct ComposeView: View {
placeholder: viewModel.statusPlaceholder, placeholder: viewModel.statusPlaceholder,
width: statusEditorViewWidth, width: statusEditorViewWidth,
attributedString: viewModel.statusContentAttributedString, attributedString: viewModel.statusContentAttributedString,
keyboardType: .twitter keyboardType: .twitter,
viewDidAppear: $viewModel.viewDidAppear
) )
.frame(width: statusEditorViewWidth) .frame(width: statusEditorViewWidth)
.frame(minHeight: 100) .frame(minHeight: 100)
@ -55,11 +56,23 @@ public struct ComposeView: View {
.listRow() .listRow()
// Attachments // Attachments
ForEach(viewModel.attachmentViewModels) { viewModel in ForEach(viewModel.attachmentViewModels) { attachmentViewModel in
let descriptionBinding = Binding {
return attachmentViewModel.descriptionContent
} set: { newValue in
attachmentViewModel.descriptionContent = newValue
}
StatusAttachmentView( StatusAttachmentView(
image: viewModel.thumbnailImage, image: attachmentViewModel.thumbnailImage,
descriptionPlaceholder: attachmentViewModel.descriptionPlaceholder,
description: descriptionBinding,
errorPrompt: attachmentViewModel.errorPrompt,
errorPromptImage: attachmentViewModel.errorPromptImage,
isUploading: attachmentViewModel.isUploading,
progressViewTintColor: attachmentViewModel.progressViewTintColor,
removeButtonAction: { removeButtonAction: {
self.viewModel.removeAttachmentViewModel(viewModel) self.viewModel.removeAttachmentViewModel(attachmentViewModel)
} }
) )
} }
@ -73,7 +86,7 @@ public struct ComposeView: View {
.listRow() .listRow()
} // end List } // end List
.introspectTableView(customize: { tableView in .introspectTableView(customize: { tableView in
tableView.keyboardDismissMode = .onDrag // tableView.keyboardDismissMode = .onDrag
tableView.verticalScrollIndicatorInsets.bottom = viewModel.toolbarHeight tableView.verticalScrollIndicatorInsets.bottom = viewModel.toolbarHeight
}) })
.preference( .preference(

View File

@ -8,12 +8,16 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine import Combine
import CoreDataStack
class ComposeViewModel: ObservableObject { class ComposeViewModel: ObservableObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
@Published var authentication: MastodonAuthentication?
@Published var toolbarHeight: CGFloat = 0 @Published var toolbarHeight: CGFloat = 0
@Published var viewDidAppear = false
@Published var avatarImageURL: URL? @Published var avatarImageURL: URL?
@Published var authorName: String = "" @Published var authorName: String = ""
@ -51,10 +55,38 @@ class ComposeViewModel: ObservableObject {
} }
.assign(to: &$characterCount) .assign(to: &$characterCount)
// setup attribute updater
$attachmentViewModels
.receive(on: DispatchQueue.main)
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.sink { attachmentViewModels in
// drive upload state
// make image upload in the queue
for attachmentViewModel in attachmentViewModels {
// skip when prefix N task when task finish OR fail OR uploading
guard let currentState = attachmentViewModel.uploadStateMachine.currentState else { break }
if currentState is StatusAttachmentViewModel.UploadState.Fail {
continue
}
if currentState is StatusAttachmentViewModel.UploadState.Finish {
continue
}
if currentState is StatusAttachmentViewModel.UploadState.Uploading {
break
}
// trigger uploading one by one
if currentState is StatusAttachmentViewModel.UploadState.Initial {
attachmentViewModel.uploadStateMachine.enter(StatusAttachmentViewModel.UploadState.Uploading.self)
break
}
}
}
.store(in: &disposeBag)
#if DEBUG #if DEBUG
avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif") // avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
authorName = "Alice" // authorName = "Alice"
authorUsername = "alice" // authorUsername = "alice"
#endif #endif
} }
@ -64,11 +96,18 @@ extension ComposeViewModel {
func setupAttachmentViewModels(_ viewModels: [StatusAttachmentViewModel]) { func setupAttachmentViewModels(_ viewModels: [StatusAttachmentViewModel]) {
attachmentViewModels = viewModels attachmentViewModels = viewModels
for viewModel in viewModels { for viewModel in viewModels {
// set delegate
viewModel.delegate = self
// set observed
viewModel.objectWillChange.sink { [weak self] _ in viewModel.objectWillChange.sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.objectWillChange.send() self.objectWillChange.send()
} }
.store(in: &viewModel.disposeBag) .store(in: &viewModel.disposeBag)
// bind authentication
$authentication
.assign(to: \.value, on: viewModel.authentication)
.store(in: &viewModel.disposeBag)
} }
} }
@ -78,3 +117,13 @@ extension ComposeViewModel {
} }
} }
} }
// MARK: - StatusAttachmentViewModelDelegate
extension ComposeViewModel: StatusAttachmentViewModelDelegate {
func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?) {
// trigger event update
DispatchQueue.main.async {
self.attachmentViewModels = self.attachmentViewModels
}
}
}

View File

@ -6,33 +6,74 @@
// //
import SwiftUI import SwiftUI
import Introspect
struct StatusAttachmentView: View { struct StatusAttachmentView: View {
let image: UIImage? let image: UIImage?
let descriptionPlaceholder: String
@Binding var description: String
let errorPrompt: String?
let errorPromptImage: UIImage
let isUploading: Bool
let progressViewTintColor: UIColor
let removeButtonAction: () -> Void let removeButtonAction: () -> Void
var body: some View { var body: some View {
let image = image ?? UIImage.placeholder(color: .systemFill) let image = image ?? UIImage.placeholder(color: .systemFill)
Color.clear ZStack(alignment: .bottom) {
.aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill) if let errorPrompt = errorPrompt {
.overlay( Color.clear
Image(uiImage: image) .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
.resizable() .overlay(
.aspectRatio(contentMode: .fill) VStack(alignment: .center) {
) Image(uiImage: errorPromptImage)
.background(Color.gray) Text(errorPrompt)
.cornerRadius(4) .lineLimit(2)
.badgeView( }
Button(action: { )
removeButtonAction() .background(Color.gray)
}, label: { } else {
Image(systemName: "minus.circle.fill") Color.clear
.renderingMode(.original) .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
.font(.system(size: 22, weight: .bold, design: .default)) .overlay(
}) Image(uiImage: image)
.buttonStyle(BorderlessButtonStyle()) .resizable()
) .aspectRatio(contentMode: .fill)
)
.background(Color.gray)
LinearGradient(gradient: Gradient(colors: [Color(white: 0, opacity: 0.69), Color.clear]), startPoint: .bottom, endPoint: .top)
.frame(maxHeight: 71)
TextField("", text: $description)
.placeholder(when: description.isEmpty) {
Text(descriptionPlaceholder).foregroundColor(Color(white: 1, opacity: 0.6))
.lineLimit(1)
}
.foregroundColor(.white)
.font(.system(size: 15, weight: .regular, design: .default))
.padding(EdgeInsets(top: 0, leading: 8, bottom: 7, trailing: 8))
}
}
.cornerRadius(4)
.badgeView(
Button(action: {
removeButtonAction()
}, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.system(size: 22, weight: .bold, design: .default))
})
.buttonStyle(BorderlessButtonStyle())
)
.overlay(
Group {
if isUploading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color(progressViewTintColor)))
}
}
)
} }
} }
@ -49,12 +90,32 @@ extension View {
} }
} }
/// ref: https://stackoverflow.com/a/57715771/3797903
extension View {
func placeholder<Content: View>(
when shouldShow: Bool,
alignment: Alignment = .leading,
@ViewBuilder placeholder: () -> Content) -> some View {
ZStack(alignment: alignment) {
placeholder().opacity(shouldShow ? 1 : 0)
self
}
}
}
struct StatusAttachmentView_Previews: PreviewProvider { struct StatusAttachmentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ScrollView { ScrollView {
StatusAttachmentView( StatusAttachmentView(
image: UIImage(systemName: "photo"), image: UIImage(systemName: "photo"),
descriptionPlaceholder: "Describe photo",
description: .constant(""),
errorPrompt: nil,
errorPromptImage: StatusAttachmentViewModel.photoFillSplitImage,
isUploading: true,
progressViewTintColor: .systemFill,
removeButtonAction: { removeButtonAction: {
// do nothing // do nothing
} }

View File

@ -0,0 +1,129 @@
//
// StatusAttachmentViewModel+UploadState.swift
// ShareActionExtension
//
// Created by MainasuK Cirno on 2021-7-20.
//
import os.log
import Foundation
import Combine
import GameplayKit
import MastodonSDK
extension StatusAttachmentViewModel {
class UploadState: GKState {
weak var viewModel: StatusAttachmentViewModel?
init(viewModel: StatusAttachmentViewModel) {
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)
viewModel?.uploadStateMachineSubject.send(self)
}
}
}
extension StatusAttachmentViewModel.UploadState {
class Initial: StatusAttachmentViewModel.UploadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard viewModel?.authentication.value != nil else { return false }
if stateClass == Initial.self {
return true
}
if viewModel?.file.value != nil {
return stateClass == Uploading.self
} else {
return stateClass == Fail.self
}
}
}
class Uploading: StatusAttachmentViewModel.UploadState {
let logger = Logger(subsystem: "StatusAttachmentViewModel.UploadState.Uploading", category: "logic")
var needsFallback = false
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Fail.self || stateClass == Finish.self || stateClass == Uploading.self
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let authentication = viewModel.authentication.value else { return }
guard let file = viewModel.file.value else { return }
let description = viewModel.descriptionContent
let query = Mastodon.API.Media.UploadMediaQuery(
file: file,
thumbnail: nil,
description: description,
focus: nil
)
let mastodonAuthenticationBox = MastodonAuthenticationBox(
domain: authentication.domain,
userID: authentication.userID,
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
)
// and needs clone the `query` if needs retry
APIService.shared.uploadMedia(
domain: mastodonAuthenticationBox.domain,
query: query,
mastodonAuthenticationBox: mastodonAuthenticationBox,
needsFallback: needsFallback
)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
switch completion {
case .failure(let error):
if let apiError = error as? Mastodon.API.Error,
apiError.httpResponseStatus == .notFound,
self.needsFallback == false
{
self.needsFallback = true
stateMachine.enter(Uploading.self)
self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fallback to V1")
} else {
self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fail: \(error.localizedDescription)")
viewModel.error = error
stateMachine.enter(Fail.self)
}
case .finished:
self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success")
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment \(response.value.id) success, \(response.value.url ?? "<nil>")")
viewModel.attachment.value = response.value
stateMachine.enter(Finish.self)
}
.store(in: &viewModel.disposeBag)
}
}
class Fail: StatusAttachmentViewModel.UploadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// allow discard publishing
return stateClass == Uploading.self || stateClass == Finish.self
}
}
class Finish: StatusAttachmentViewModel.UploadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return false
}
}
}

View File

@ -9,16 +9,29 @@ import os.log
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine import Combine
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonUI import MastodonUI
import AVFoundation import AVFoundation
import GameplayKit
import MobileCoreServices import MobileCoreServices
import UniformTypeIdentifiers import UniformTypeIdentifiers
protocol StatusAttachmentViewModelDelegate: AnyObject {
func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?)
}
final class StatusAttachmentViewModel: ObservableObject, Identifiable { final class StatusAttachmentViewModel: ObservableObject, Identifiable {
static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
static let videoSplashImage: UIImage = {
let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
return image
}()
let logger = Logger(subsystem: "StatusAttachmentViewModel", category: "logic") let logger = Logger(subsystem: "StatusAttachmentViewModel", category: "logic")
weak var delegate: StatusAttachmentViewModelDelegate?
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
let id = UUID() let id = UUID()
@ -26,15 +39,36 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable {
// input // input
let file = CurrentValueSubject<Mastodon.Query.MediaAttachment?, Never>(nil) let file = CurrentValueSubject<Mastodon.Query.MediaAttachment?, Never>(nil)
@Published var description = "" let authentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
@Published var descriptionContent = ""
// output // output
let attachment = CurrentValueSubject<Mastodon.Entity.Attachment?, Never>(nil)
@Published var thumbnailImage: UIImage? @Published var thumbnailImage: UIImage?
@Published var descriptionPlaceholder = ""
@Published var isUploading = true
@Published var progressViewTintColor = UIColor.systemFill
@Published var error: Error? @Published var error: Error?
@Published var errorPrompt: String?
@Published var errorPromptImage: UIImage = StatusAttachmentViewModel.photoFillSplitImage
private(set) lazy var uploadStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
UploadState.Initial(viewModel: self),
UploadState.Uploading(viewModel: self),
UploadState.Fail(viewModel: self),
UploadState.Finish(viewModel: self),
])
stateMachine.enter(UploadState.Initial.self)
return stateMachine
}()
lazy var uploadStateMachineSubject = CurrentValueSubject<StatusAttachmentViewModel.UploadState?, Never>(nil)
init(itemProvider: NSItemProvider) { init(itemProvider: NSItemProvider) {
self.itemProvider = itemProvider self.itemProvider = itemProvider
// bind attachment from item provider
Just(itemProvider) Just(itemProvider)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.flatMap { result -> AnyPublisher<Mastodon.Query.MediaAttachment?, Error> in .flatMap { result -> AnyPublisher<Mastodon.Query.MediaAttachment?, Error> in
@ -51,18 +85,49 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable {
switch completion { switch completion {
case .failure(let error): case .failure(let error):
self.error = error self.error = error
// self.uploadStateMachine.enter(UploadState.Fail.self) self.uploadStateMachine.enter(UploadState.Fail.self)
case .finished: case .finished:
break break
} }
} receiveValue: { [weak self] file in } receiveValue: { [weak self] file in
guard let self = self else { return } guard let self = self else { return }
self.file.value = file self.file.value = file
// self.uploadStateMachine.enter(UploadState.Initial.self) self.uploadStateMachine.enter(UploadState.Initial.self)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// bind progress view tint color
$thumbnailImage
.receive(on: DispatchQueue.main)
.map { image -> UIColor in
guard let image = image else { return .systemFill }
switch image.domainLumaCoefficientsStyle {
case .light:
return UIColor.black.withAlphaComponent(0.8)
default:
return UIColor.white.withAlphaComponent(0.8)
}
}
.assign(to: &$progressViewTintColor)
// bind description placeholder and error prompt image
file
.receive(on: DispatchQueue.main)
.sink { [weak self] file in
guard let self = self else { return }
guard let file = file else { return }
switch file {
case .jpeg, .png, .gif:
self.descriptionPlaceholder = L10n.Scene.Compose.Attachment.descriptionPhoto
self.errorPromptImage = StatusAttachmentViewModel.photoFillSplitImage
case .other:
self.descriptionPlaceholder = L10n.Scene.Compose.Attachment.descriptionVideo
self.errorPromptImage = StatusAttachmentViewModel.videoSplashImage
}
}
.store(in: &disposeBag)
// bind thumbnail image
file file
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.map { file -> UIImage? in .map { file -> UIImage? in
@ -92,6 +157,56 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable {
} }
} }
.assign(to: &$thumbnailImage) .assign(to: &$thumbnailImage)
// bind state and error
Publishers.CombineLatest(
uploadStateMachineSubject,
$error
)
.sink { [weak self] state, error in
guard let self = self else { return }
// trigger delegate
self.delegate?.statusAttachmentViewModel(self, uploadStateDidChange: state)
// set error prompt
if let error = error {
self.isUploading = false
self.errorPrompt = error.localizedDescription
} else {
guard let state = state else { return }
switch state {
case is UploadState.Finish:
self.isUploading = false
case is UploadState.Fail:
self.isUploading = false
// FIXME: not display
self.errorPrompt = {
guard let file = self.file.value else {
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
}
switch file {
case .jpeg, .png, .gif:
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
case .other:
return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
}
}()
default:
break
}
}
}
.store(in: &disposeBag)
// trigger delegate when authentication get new value
authentication
.receive(on: DispatchQueue.main)
.sink { [weak self] authentication in
guard let self = self else { return }
guard authentication != nil else { return }
self.delegate?.statusAttachmentViewModel(self, uploadStateDidChange: self.uploadStateMachineSubject.value)
}
.store(in: &disposeBag)
} }
} }

View File

@ -21,11 +21,12 @@ struct StatusAuthorView: View {
HStack(spacing: 5) { HStack(spacing: 5) {
AnimatedImage(imageURL: avatarImageURL) AnimatedImage(imageURL: avatarImageURL)
.frame(width: 42, height: 42) .frame(width: 42, height: 42)
.background(Color(UIColor.systemFill))
.cornerRadius(4) .cornerRadius(4)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(name) Text(name)
.font(.headline) .font(.headline)
Text("@" + username) Text(username)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }

View File

@ -16,19 +16,22 @@ public struct StatusEditorView: UIViewRepresentable {
let width: CGFloat let width: CGFloat
let attributedString: NSAttributedString let attributedString: NSAttributedString
let keyboardType: UIKeyboardType let keyboardType: UIKeyboardType
@Binding var viewDidAppear: Bool
public init( public init(
string: Binding<String>, string: Binding<String>,
placeholder: String, placeholder: String,
width: CGFloat, width: CGFloat,
attributedString: NSAttributedString, attributedString: NSAttributedString,
keyboardType: UIKeyboardType keyboardType: UIKeyboardType,
viewDidAppear: Binding<Bool>
) { ) {
self._string = string self._string = string
self.placeholder = placeholder self.placeholder = placeholder
self.width = width self.width = width
self.attributedString = attributedString self.attributedString = attributedString
self.keyboardType = keyboardType self.keyboardType = keyboardType
self._viewDidAppear = viewDidAppear
} }
public func makeUIView(context: Context) -> UITextView { public func makeUIView(context: Context) -> UITextView {
@ -45,6 +48,7 @@ public struct StatusEditorView: UIViewRepresentable {
let widthLayoutConstraint = textView.widthAnchor.constraint(equalToConstant: 100) let widthLayoutConstraint = textView.widthAnchor.constraint(equalToConstant: 100)
widthLayoutConstraint.priority = .required - 1 widthLayoutConstraint.priority = .required - 1
context.coordinator.widthLayoutConstraint = widthLayoutConstraint context.coordinator.widthLayoutConstraint = widthLayoutConstraint
return textView return textView
} }
@ -55,6 +59,12 @@ public struct StatusEditorView: UIViewRepresentable {
// update layout // update layout
context.coordinator.updateLayout(width: width) context.coordinator.updateLayout(width: width)
// set becomeFirstResponder
if viewDidAppear {
viewDidAppear = false
textView.becomeFirstResponder()
}
} }
public func makeCoordinator() -> Coordinator { public func makeCoordinator() -> Coordinator {

View File

@ -0,0 +1,32 @@
//
// APIService.swift
// ShareActionExtension
//
// Created by MainasuK Cirno on 2021-7-20.
//
import os.log
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
// Replica APIService for share extension
final class APIService {
var disposeBag = Set<AnyCancellable>()
static let shared = APIService()
// internal
let session: URLSession
// output
let error = PassthroughSubject<APIError, Never>()
private init() {
self.session = URLSession(configuration: .default)
}
}