forked from zelo72/mastodon-ios
feat: complete upload and publish logic
This commit is contained in:
parent
1cdbd7fa2a
commit
d2f9828f50
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -21,7 +21,6 @@ final class APIService {
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
let session: URLSession
|
let session: URLSession
|
||||||
|
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let backgroundManagedObjectContext: NSManagedObjectContext
|
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue