feat: complete upload and publish logic
This commit is contained in:
parent
1cdbd7fa2a
commit
d2f9828f50
|
@ -22,7 +22,7 @@
|
|||
"publish_post_failure": {
|
||||
"title": "Publish Failure",
|
||||
"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.",
|
||||
"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 */; };
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.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 */; };
|
||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.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 */; };
|
||||
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.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 */; };
|
||||
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9426157E6E0045B23D /* APIService+Relationship.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 */; };
|
||||
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -727,7 +738,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1204,7 +1214,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1242,6 +1251,10 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1341,6 +1354,7 @@
|
|||
DB41ED8026A54D7C00F58330 /* AlamofireImage in Frameworks */,
|
||||
DBC6463326A195DB00B0E31B /* AppShared.framework in Frameworks */,
|
||||
4278334D6033AEEE0A1C5155 /* Pods_ShareActionExtension.framework in Frameworks */,
|
||||
DBFEF07126A690E8006D7ED1 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -2055,6 +2069,7 @@
|
|||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
||||
DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */,
|
||||
DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */,
|
||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||
2D61254C262547C200299647 /* APIService+Notification.swift */,
|
||||
|
@ -2497,8 +2512,6 @@
|
|||
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */,
|
||||
0FAA101B25E10E760017CCDE /* UIFont.swift */,
|
||||
2D939AB425EDD8A90076FA61 /* String.swift */,
|
||||
2D206B7F25F5F45E00143C56 /* UIImage.swift */,
|
||||
DBCC3B88261454BA0045B23D /* CGImage.swift */,
|
||||
2D206B8525F5FB0900143C56 /* Double.swift */,
|
||||
2D206B9125F60EA700143C56 /* UIControl.swift */,
|
||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
||||
|
@ -2764,6 +2777,7 @@
|
|||
DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */,
|
||||
DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */,
|
||||
DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */,
|
||||
DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2791,6 +2805,7 @@
|
|||
DBC6461926A170AB00B0E31B /* Info.plist */,
|
||||
DBC6461626A170AB00B0E31B /* MainInterface.storyboard */,
|
||||
DBFEF06126A57721006D7ED1 /* Scene */,
|
||||
DBFEF07426A69140006D7ED1 /* Service */,
|
||||
);
|
||||
path = ShareActionExtension;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2902,6 +2917,7 @@
|
|||
DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */,
|
||||
DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */,
|
||||
DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */,
|
||||
DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2916,6 +2932,14 @@
|
|||
path = Scene;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBFEF07426A69140006D7ED1 /* Service */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBFEF07226A6913D006D7ED1 /* APIService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -3111,6 +3135,7 @@
|
|||
DB41ED7F26A54D7C00F58330 /* AlamofireImage */,
|
||||
DB41ED8126A54D8A00F58330 /* MastodonMeta */,
|
||||
DB41ED8326A54D8A00F58330 /* MetaTextView */,
|
||||
DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */,
|
||||
);
|
||||
productName = ShareActionExtension;
|
||||
productReference = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */;
|
||||
|
@ -3821,6 +3846,7 @@
|
|||
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */,
|
||||
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */,
|
||||
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
|
@ -3836,7 +3862,6 @@
|
|||
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */,
|
||||
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */,
|
||||
0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */,
|
||||
DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */,
|
||||
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
|
||||
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */,
|
||||
|
@ -3871,6 +3896,7 @@
|
|||
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
|
||||
DBBC24B526A540AE00398BB9 /* AvatarConfigurableView.swift in Sources */,
|
||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||
DBFEF07726A691FB006D7ED1 /* MastodonAuthenticationBox.swift in Sources */,
|
||||
DBBC24CF26A547AE00398BB9 /* ThemeService+Appearance.swift in Sources */,
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
|
||||
|
@ -3910,7 +3936,6 @@
|
|||
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
|
||||
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */,
|
||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */,
|
||||
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
||||
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
||||
|
@ -3993,21 +4018,28 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */,
|
||||
DBFEF07326A6913D006D7ED1 /* APIService.swift in Sources */,
|
||||
DB41ED7C26A54D5500F58330 /* MastodonStatusContent+Appearance.swift in Sources */,
|
||||
DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */,
|
||||
DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */,
|
||||
DBBC24B326A53EE700398BB9 /* ActiveLabel.swift in Sources */,
|
||||
DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */,
|
||||
DBBC24CB26A546C000398BB9 /* ThemePreference.swift in Sources */,
|
||||
DBFEF05F26A57715006D7ED1 /* StatusAuthorView.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 */,
|
||||
DB41ED8A26A54F4C00F58330 /* AttachmentContainerView.swift in Sources */,
|
||||
DBFEF06F26A690C4006D7ED1 /* APIService+APIError.swift in Sources */,
|
||||
DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */,
|
||||
DBBC24C726A5456400398BB9 /* SystemTheme.swift in Sources */,
|
||||
DBBC24B626A5419700398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */,
|
||||
DBC6462926A1736700B0E31B /* Strings.swift in Sources */,
|
||||
DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */,
|
||||
DBFEF06826A67DEE006D7ED1 /* MastodonUser.swift in Sources */,
|
||||
DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */,
|
||||
DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */,
|
||||
DBBC24B926A5426000398BB9 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
DB41ED8B26A54F5800F58330 /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||
DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */,
|
||||
|
@ -4019,6 +4051,9 @@
|
|||
DB41ED8926A54F4000F58330 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
|
||||
DBBC24D226A5488600398BB9 /* AvatarConfigurableView.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 */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -5579,6 +5614,11 @@
|
|||
package = DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */;
|
||||
productName = FPSIndicator;
|
||||
};
|
||||
DBFEF07026A690E8006D7ED1 /* AlamofireNetworkActivityIndicator */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */;
|
||||
productName = AlamofireNetworkActivityIndicator;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency 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")
|
||||
/// Publish Failure
|
||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title")
|
||||
internal enum AttchmentsMessage {
|
||||
internal enum AttachmentsMessage {
|
||||
/// 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.
|
||||
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 {
|
||||
|
|
|
@ -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(
|
||||
context: AppContext,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
activeMastodonAuthenticationBox: MastodonAuthenticationBox,
|
||||
mastodonUser: AnyPublisher<MastodonUser?, Never>
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
mastodonUser
|
||||
|
@ -111,7 +111,7 @@ extension UserProviderFacade {
|
|||
|
||||
private static func _toggleUserBlockRelationship(
|
||||
context: AppContext,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
activeMastodonAuthenticationBox: MastodonAuthenticationBox,
|
||||
mastodonUser: AnyPublisher<MastodonUser?, Never>
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
mastodonUser
|
||||
|
@ -174,7 +174,7 @@ extension UserProviderFacade {
|
|||
|
||||
private static func _toggleUserMuteRelationship(
|
||||
context: AppContext,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
activeMastodonAuthenticationBox: MastodonAuthenticationBox,
|
||||
mastodonUser: AnyPublisher<MastodonUser?, Never>
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
mastodonUser
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
"Common.Alerts.DiscardPostContent.Title" = "Discard Draft";
|
||||
"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again.";
|
||||
"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error";
|
||||
"Common.Alerts.PublishPostFailure.AttchmentsMessage.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.MoreThanOneVideo" = "Cannot attach more than one video.";
|
||||
"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.
|
||||
Please check your internet connection.";
|
||||
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
"Common.Alerts.DiscardPostContent.Title" = "Discard Draft";
|
||||
"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again.";
|
||||
"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error";
|
||||
"Common.Alerts.PublishPostFailure.AttchmentsMessage.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.MoreThanOneVideo" = "Cannot attach more than one video.";
|
||||
"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.
|
||||
Please check your internet connection.";
|
||||
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
|
||||
|
|
|
@ -28,7 +28,7 @@ final class ComposeViewModel: NSObject {
|
|||
let isContentWarningComposing = CurrentValueSubject<Bool, Never>(false)
|
||||
let selectedStatusVisibility: CurrentValueSubject<ComposeToolbarView.VisibilitySelectionType, 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 repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||
let autoCompleteRetryLayoutTimes = CurrentValueSubject<Int, Never>(0)
|
||||
|
@ -202,13 +202,13 @@ final class ComposeViewModel: NSObject {
|
|||
}
|
||||
.assign(to: \.value, on: characterCount)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind compose bar button item UI state
|
||||
let isComposeContentEmpty = composeStatusAttribute.composeContent
|
||||
.map { ($0 ?? "").isEmpty }
|
||||
let isComposeContentValid = composeStatusAttribute.composeContent
|
||||
.map { composeContent -> Bool in
|
||||
let composeContent = composeContent ?? ""
|
||||
return composeContent.count <= ComposeViewModel.composeContentLimit
|
||||
let isComposeContentValid = characterCount
|
||||
.map { characterCount -> Bool in
|
||||
return characterCount <= ComposeViewModel.composeContentLimit
|
||||
}
|
||||
let isMediaEmpty = attachmentServices
|
||||
.map { $0.isEmpty }
|
||||
|
@ -224,10 +224,10 @@ final class ComposeViewModel: NSObject {
|
|||
}
|
||||
|
||||
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
|
||||
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||
isComposeContentValid.eraseToAnyPublisher(),
|
||||
isMediaEmpty.eraseToAnyPublisher(),
|
||||
isMediaUploadAllSuccess.eraseToAnyPublisher()
|
||||
isComposeContentEmpty,
|
||||
isComposeContentValid,
|
||||
isMediaEmpty,
|
||||
isMediaUploadAllSuccess
|
||||
)
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
|
||||
if isMediaEmpty {
|
||||
|
@ -239,10 +239,10 @@ final class ComposeViewModel: NSObject {
|
|||
.eraseToAnyPublisher()
|
||||
|
||||
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
|
||||
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||
isComposeContentValid.eraseToAnyPublisher(),
|
||||
isPollComposing.eraseToAnyPublisher(),
|
||||
isPollAttributeAllValid.eraseToAnyPublisher()
|
||||
isComposeContentEmpty,
|
||||
isComposeContentValid,
|
||||
isPollComposing,
|
||||
isPollAttributeAllValid
|
||||
)
|
||||
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
|
||||
if isPollComposing {
|
||||
|
@ -390,9 +390,9 @@ extension ComposeViewModel {
|
|||
var failureReason: String? {
|
||||
switch self {
|
||||
case .videoAttachWithPhoto:
|
||||
return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.videoAttachWithPhoto
|
||||
return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
|
||||
case .moreThanOneVideo:
|
||||
return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.moreThanOneVideo
|
||||
return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,9 +116,11 @@ extension ComposeStatusAttachmentTableViewCell {
|
|||
} else {
|
||||
guard let uploadState = uploadState else { return }
|
||||
switch uploadState {
|
||||
case is MastodonAttachmentService.UploadState.Finish,
|
||||
is MastodonAttachmentService.UploadState.Fail:
|
||||
case is MastodonAttachmentService.UploadState.Finish:
|
||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||
case is MastodonAttachmentService.UploadState.Fail:
|
||||
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||
// FIXME: not display
|
||||
cell.attachmentContainerView.emptyStateView.label.text = {
|
||||
if let file = attachmentService.file.value {
|
||||
switch file {
|
||||
|
|
|
@ -26,7 +26,7 @@ final class NotificationViewModel: NSObject {
|
|||
let selectedIndex = CurrentValueSubject<NotificationSegment, Never>(.EveryThing)
|
||||
let noMoreNotification = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
|
||||
let fetchedResultsController: NSFetchedResultsController<MastodonNotification>!
|
||||
let notificationPredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||
let cellFrameCache = NSCache<NSString, NSValue>()
|
||||
|
|
|
@ -17,7 +17,7 @@ final class FavoriteViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
let activeMastodonAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ class ProfileViewModel: NSObject {
|
|||
context.authenticationService.activeMastodonAuthenticationBox.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 mastodonUserID != activeMastodonAuthenticationBox.userID else { return nil }
|
||||
return (mastodonUserID, activeMastodonAuthenticationBox)
|
||||
|
|
|
@ -17,7 +17,7 @@ extension ReportViewModel {
|
|||
func requestRecentStatus(
|
||||
domain: String,
|
||||
accountId: String,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) {
|
||||
context.apiService.userTimeline(
|
||||
domain: domain,
|
||||
|
|
|
@ -165,7 +165,7 @@ class ReportViewModel: NSObject {
|
|||
.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
|
||||
guard let self = self else { return value }
|
||||
self.reportQuery.comment = nil
|
||||
|
|
|
@ -42,7 +42,7 @@ final class SearchViewModel: NSObject {
|
|||
context.authenticationService.activeMastodonAuthenticationBox,
|
||||
viewDidAppeared
|
||||
)
|
||||
.compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in
|
||||
.compactMap { activeMastodonAuthenticationBox, _ -> MastodonAuthenticationBox? in
|
||||
return activeMastodonAuthenticationBox
|
||||
}
|
||||
.throttle(for: 1, scheduler: DispatchQueue.main, latest: false)
|
||||
|
@ -72,7 +72,7 @@ final class SearchViewModel: NSObject {
|
|||
context.authenticationService.activeMastodonAuthenticationBox,
|
||||
viewDidAppeared
|
||||
)
|
||||
.compactMap { activeMastodonAuthenticationBox, _ -> AuthenticationService.MastodonAuthenticationBox? in
|
||||
.compactMap { activeMastodonAuthenticationBox, _ -> MastodonAuthenticationBox? in
|
||||
return activeMastodonAuthenticationBox
|
||||
}
|
||||
.throttle(for: 1, scheduler: DispatchQueue.main, latest: false)
|
||||
|
|
|
@ -16,7 +16,7 @@ extension APIService {
|
|||
|
||||
func toggleBlock(
|
||||
for mastodonUser: MastodonUser,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
activeMastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||
|
@ -86,7 +86,7 @@ extension APIService {
|
|||
// update database local and return block query update type for remote request
|
||||
func blockUpdateLocal(
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<(Mastodon.API.Account.BlockQueryType, MastodonUser.ID), Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
@ -132,7 +132,7 @@ extension APIService {
|
|||
func blockUpdateRemote(
|
||||
blockQueryType: Mastodon.API.Account.BlockQueryType,
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -17,7 +17,7 @@ extension APIService {
|
|||
func getDomainblocks(
|
||||
domain: String,
|
||||
limit: Int = onceRequestDomainBlocksMaxCount,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[String]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
|
||||
|
@ -71,7 +71,7 @@ extension APIService {
|
|||
|
||||
func blockDomain(
|
||||
user: MastodonUser,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
|
||||
|
@ -105,7 +105,7 @@ extension APIService {
|
|||
|
||||
func unblockDomain(
|
||||
user: MastodonUser,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Empty>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ extension APIService {
|
|||
func favorite(
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
favoriteKind: Mastodon.API.Favorites.FavoriteKind,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
@ -139,7 +139,7 @@ extension APIService {
|
|||
func favoritedStatuses(
|
||||
limit: Int = onceRequestStatusMaxCount,
|
||||
maxID: String? = nil,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
|
|
@ -15,7 +15,7 @@ import MastodonSDK
|
|||
extension APIService {
|
||||
|
||||
func filters(
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
|
|
|
@ -24,7 +24,7 @@ extension APIService {
|
|||
/// - Returns: publisher for `Relationship`
|
||||
func toggleFollow(
|
||||
for mastodonUser: MastodonUser,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
activeMastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
|
||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
|
@ -96,7 +96,7 @@ extension APIService {
|
|||
// update database local and return follow query update type for remote request
|
||||
func followUpdateLocal(
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<(Mastodon.API.Account.FollowQueryType, MastodonUser.ID), Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
@ -156,7 +156,7 @@ extension APIService {
|
|||
func followUpdateRemote(
|
||||
followQueryType: Mastodon.API.Account.FollowQueryType,
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -17,7 +17,7 @@ import MastodonSDK
|
|||
extension APIService {
|
||||
func acceptFollowRequest(
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
@ -61,7 +61,7 @@ extension APIService {
|
|||
|
||||
func rejectFollowRequest(
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -22,7 +22,7 @@ extension APIService {
|
|||
limit: Int = onceRequestStatusMaxCount,
|
||||
local: Bool? = nil,
|
||||
hashtag: String,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
|
|
|
@ -21,7 +21,7 @@ extension APIService {
|
|||
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||
limit: Int = onceRequestStatusMaxCount,
|
||||
local: Bool? = nil,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
|
|
|
@ -14,7 +14,7 @@ extension APIService {
|
|||
func uploadMedia(
|
||||
domain: String,
|
||||
query: Mastodon.API.Media.UploadMediaQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox,
|
||||
needsFallback: Bool
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
if needsFallback {
|
||||
|
@ -27,7 +27,7 @@ extension APIService {
|
|||
private func uploadMediaV1(
|
||||
domain: String,
|
||||
query: Mastodon.API.Media.UploadMediaQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
@ -42,7 +42,7 @@ extension APIService {
|
|||
private func uploadMediaV2(
|
||||
domain: String,
|
||||
query: Mastodon.API.Media.UploadMediaQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
@ -54,12 +54,16 @@ extension APIService {
|
|||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService {
|
||||
|
||||
func updateMedia(
|
||||
domain: String,
|
||||
attachmentID: Mastodon.Entity.Attachment.ID,
|
||||
query: Mastodon.API.Media.UpdateMediaQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ extension APIService {
|
|||
|
||||
func toggleMute(
|
||||
for mastodonUser: MastodonUser,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
activeMastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||
|
@ -86,7 +86,7 @@ extension APIService {
|
|||
// update database local and return mute query update type for remote request
|
||||
func muteUpdateLocal(
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<(Mastodon.API.Account.MuteQueryType, MastodonUser.ID), Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
@ -132,7 +132,7 @@ extension APIService {
|
|||
func muteUpdateRemote(
|
||||
muteQueryType: Mastodon.API.Account.MuteQueryType,
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -16,7 +16,7 @@ extension APIService {
|
|||
func allNotifications(
|
||||
domain: String,
|
||||
query: Mastodon.API.Notifications.Query,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let userID = mastodonAuthenticationBox.userID
|
||||
|
@ -75,7 +75,7 @@ extension APIService {
|
|||
|
||||
func notification(
|
||||
notificationID: Mastodon.Entity.Notification.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -19,7 +19,7 @@ extension APIService {
|
|||
domain: String,
|
||||
pollID: Mastodon.Entity.Poll.ID,
|
||||
pollObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
@ -143,7 +143,7 @@ extension APIService {
|
|||
pollID: Mastodon.Entity.Poll.ID,
|
||||
pollObjectID: NSManagedObjectID,
|
||||
choices: [Int],
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
|
|
@ -62,7 +62,7 @@ extension APIService {
|
|||
func reblog(
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
reblogKind: Mastodon.API.Reblog.ReblogKind,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
|
|
@ -16,7 +16,7 @@ extension APIService {
|
|||
func suggestionAccount(
|
||||
domain: String,
|
||||
query: Mastodon.API.Suggestions.Query?,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
@ -47,7 +47,7 @@ extension APIService {
|
|||
func suggestionAccountV2(
|
||||
domain: String,
|
||||
query: Mastodon.API.Suggestions.Query?,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.V2.SuggestionAccount]>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ extension APIService {
|
|||
func relationship(
|
||||
domain: String,
|
||||
accountIDs: [Mastodon.Entity.Account.ID],
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
|
|
|
@ -14,7 +14,7 @@ extension APIService {
|
|||
func report(
|
||||
domain: String,
|
||||
query: Mastodon.API.Reports.FileReportQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Bool>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ extension APIService {
|
|||
func search(
|
||||
domain: String,
|
||||
query: Mastodon.API.V2.Search.Query,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
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
|
||||
|
||||
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(
|
||||
domain: String,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
return Mastodon.API.Statuses.status(
|
||||
|
@ -91,7 +54,7 @@ extension APIService {
|
|||
func deleteStatus(
|
||||
domain: String,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let query = Mastodon.API.Statuses.DeleteStatusQuery(id: statusID)
|
||||
|
|
|
@ -17,7 +17,7 @@ extension APIService {
|
|||
func createSubscription(
|
||||
subscriptionObjectID: NSManagedObjectID,
|
||||
query: Mastodon.API.Subscriptions.CreateSubscriptionQuery,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Subscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
|
@ -50,7 +50,7 @@ extension APIService {
|
|||
}
|
||||
|
||||
func cancelSubscription(
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.EmptySubscription>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
|
|
|
@ -17,7 +17,7 @@ extension APIService {
|
|||
func statusContext(
|
||||
domain: String,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Context>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
guard domain == mastodonAuthenticationBox.domain else {
|
||||
|
|
|
@ -23,7 +23,7 @@ extension APIService {
|
|||
excludeReplies: Bool? = nil,
|
||||
excludeReblogs: Bool? = nil,
|
||||
onlyMedia: Bool? = nil,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
|
|
|
@ -21,7 +21,6 @@ final class APIService {
|
|||
|
||||
// internal
|
||||
let session: URLSession
|
||||
|
||||
|
||||
// input
|
||||
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||
|
|
|
@ -24,9 +24,9 @@ final class AuthenticationService: NSObject {
|
|||
|
||||
// output
|
||||
let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([])
|
||||
let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
|
||||
let mastodonAuthenticationBoxes = CurrentValueSubject<[MastodonAuthenticationBox], Never>([])
|
||||
let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
|
||||
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(nil)
|
||||
let activeMastodonAuthenticationBox = CurrentValueSubject<MastodonAuthenticationBox?, Never>(nil)
|
||||
|
||||
init(
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
|
@ -61,11 +61,11 @@ final class AuthenticationService: NSObject {
|
|||
.store(in: &disposeBag)
|
||||
|
||||
mastodonAuthentications
|
||||
.map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in
|
||||
.map { authentications -> [MastodonAuthenticationBox] in
|
||||
return authentications
|
||||
.sorted(by: { $0.activedAt > $1.activedAt })
|
||||
.compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in
|
||||
return AuthenticationService.MastodonAuthenticationBox(
|
||||
.compactMap { authentication -> MastodonAuthenticationBox? in
|
||||
return MastodonAuthenticationBox(
|
||||
domain: authentication.domain,
|
||||
userID: authentication.userID,
|
||||
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 {
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
_mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox(
|
||||
_mastodonAuthenticationBox = MastodonAuthenticationBox(
|
||||
domain: mastodonAuthentication.domain,
|
||||
userID: mastodonAuthentication.userID,
|
||||
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken),
|
||||
|
|
|
@ -83,7 +83,7 @@ extension MastodonAttachmentService.UploadState {
|
|||
{
|
||||
self.needsFallback = true
|
||||
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 {
|
||||
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)
|
||||
|
|
|
@ -27,7 +27,7 @@ final class MastodonAttachmentService {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
var authenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||
var authenticationBox: MastodonAuthenticationBox?
|
||||
let file = CurrentValueSubject<Mastodon.Query.MediaAttachment?, Never>(nil)
|
||||
let description = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
|
@ -52,7 +52,7 @@ final class MastodonAttachmentService {
|
|||
init(
|
||||
context: AppContext,
|
||||
pickerResult: PHPickerResult,
|
||||
initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||
initialAuthenticationBox: MastodonAuthenticationBox?
|
||||
) {
|
||||
self.context = context
|
||||
self.authenticationBox = initialAuthenticationBox
|
||||
|
@ -90,7 +90,7 @@ final class MastodonAttachmentService {
|
|||
init(
|
||||
context: AppContext,
|
||||
image: UIImage,
|
||||
initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||
initialAuthenticationBox: MastodonAuthenticationBox?
|
||||
) {
|
||||
self.context = context
|
||||
self.authenticationBox = initialAuthenticationBox
|
||||
|
@ -105,7 +105,7 @@ final class MastodonAttachmentService {
|
|||
init(
|
||||
context: AppContext,
|
||||
documentURL: URL,
|
||||
initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||
initialAuthenticationBox: MastodonAuthenticationBox?
|
||||
) {
|
||||
self.context = context
|
||||
self.authenticationBox = initialAuthenticationBox
|
||||
|
@ -191,7 +191,7 @@ extension MastodonAttachmentService {
|
|||
|
||||
extension MastodonAttachmentService {
|
||||
// FIXME: needs reset state for multiple account posting support
|
||||
func uploading(mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> Bool {
|
||||
func uploading(mastodonAuthenticationBox: MastodonAuthenticationBox) -> Bool {
|
||||
authenticationBox = mastodonAuthenticationBox
|
||||
return uploadStateMachine.enter(UploadState.self)
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ extension NotificationService {
|
|||
extension NotificationService {
|
||||
|
||||
func dequeueNotificationViewModel(
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> NotificationViewModel? {
|
||||
var _notificationSubscription: NotificationViewModel?
|
||||
workingQueue.sync {
|
||||
|
@ -130,7 +130,7 @@ extension NotificationService {
|
|||
|
||||
// cancel subscription if sign-out
|
||||
let accessToken = mastodonPushNotification.accessToken
|
||||
let mastodonAuthenticationBox = AuthenticationService.MastodonAuthenticationBox(
|
||||
let mastodonAuthenticationBox = MastodonAuthenticationBox(
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
appAuthorization: .init(accessToken: accessToken),
|
||||
|
@ -178,7 +178,7 @@ extension NotificationService.NotificationViewModel {
|
|||
func createSubscribeQuery(
|
||||
deviceToken: Data,
|
||||
queryData: Mastodon.API.Subscriptions.QueryData,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> Mastodon.API.Subscriptions.CreateSubscriptionQuery {
|
||||
let deviceToken = [UInt8](deviceToken).toHexString()
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ final class SettingService {
|
|||
|
||||
// create setting (if non-exist) for authenticated users
|
||||
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 authenticationService = self.authenticationService else { return nil }
|
||||
guard let activeMastodonAuthenticationBox = mastodonAuthenticationBoxes.first else { return nil }
|
||||
|
|
|
@ -104,7 +104,7 @@ extension StatusPrefetchingService {
|
|||
statusObjectID: NSManagedObjectID,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
replyToStatusID: Mastodon.Entity.Status.ID,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) {
|
||||
workingQueue.async { [weak self] in
|
||||
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
|
||||
//
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-16.
|
||||
// Created by sxiaojian on 2021/3/8.
|
||||
//
|
||||
|
||||
import CoreImage
|
||||
import CoreImage.CIFilterBuiltins
|
||||
import UIKit
|
||||
|
||||
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 {
|
||||
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 = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: 4096,
|
||||
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize,
|
||||
] as CFDictionary
|
||||
|
||||
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 publishBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||
barButtonItem.target = self
|
||||
barButtonItem.action = #selector(ShareViewController.publishBarButtonItemPressed(_:))
|
||||
publishButton.addTarget(self, action: #selector(ShareViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
|
@ -173,6 +172,12 @@ extension ShareViewController {
|
|||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind valid
|
||||
viewModel.isValid
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: publishButton)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -180,6 +185,8 @@ extension ShareViewController {
|
|||
|
||||
viewModel.viewDidAppear.value = true
|
||||
viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
|
||||
|
||||
viewModel.composeViewModel.viewDidAppear = true
|
||||
}
|
||||
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
|
@ -204,6 +211,42 @@ extension ShareViewController {
|
|||
|
||||
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
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 CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
@ -33,6 +34,7 @@ final class ShareViewModel {
|
|||
// output
|
||||
let authentication = CurrentValueSubject<Result<MastodonAuthentication, Error>?, Never>(nil)
|
||||
let isFetchAuthentication = CurrentValueSubject<Bool, Never>(true)
|
||||
let isPublishing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isBusy = CurrentValueSubject<Bool, Never>(true)
|
||||
let isValid = CurrentValueSubject<Bool, Never>(false)
|
||||
let composeViewModel = ComposeViewModel()
|
||||
|
@ -59,11 +61,13 @@ final class ShareViewModel {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind authentication loading state
|
||||
authentication
|
||||
.map { result in result == nil }
|
||||
.assign(to: \.value, on: isFetchAuthentication)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind user locked state
|
||||
authentication
|
||||
.compactMap { result -> Bool? in
|
||||
guard let result = result else { return nil }
|
||||
|
@ -80,15 +84,105 @@ final class ShareViewModel {
|
|||
.assign(to: \.value, on: selectedStatusVisibility)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
isFetchAuthentication
|
||||
// bind author
|
||||
authentication
|
||||
.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)
|
||||
|
||||
// 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.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder
|
||||
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)
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -97,10 +191,6 @@ final class ShareViewModel {
|
|||
self.setupBackgroundColor(theme: theme)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
composeViewModel.$characterCount
|
||||
.assign(to: \.value, on: characterCount)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
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,
|
||||
width: statusEditorViewWidth,
|
||||
attributedString: viewModel.statusContentAttributedString,
|
||||
keyboardType: .twitter
|
||||
keyboardType: .twitter,
|
||||
viewDidAppear: $viewModel.viewDidAppear
|
||||
)
|
||||
.frame(width: statusEditorViewWidth)
|
||||
.frame(minHeight: 100)
|
||||
|
@ -55,11 +56,23 @@ public struct ComposeView: View {
|
|||
.listRow()
|
||||
|
||||
// Attachments
|
||||
ForEach(viewModel.attachmentViewModels) { viewModel in
|
||||
ForEach(viewModel.attachmentViewModels) { attachmentViewModel in
|
||||
let descriptionBinding = Binding {
|
||||
return attachmentViewModel.descriptionContent
|
||||
} set: { newValue in
|
||||
attachmentViewModel.descriptionContent = newValue
|
||||
}
|
||||
|
||||
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: {
|
||||
self.viewModel.removeAttachmentViewModel(viewModel)
|
||||
self.viewModel.removeAttachmentViewModel(attachmentViewModel)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -73,7 +86,7 @@ public struct ComposeView: View {
|
|||
.listRow()
|
||||
} // end List
|
||||
.introspectTableView(customize: { tableView in
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
// tableView.keyboardDismissMode = .onDrag
|
||||
tableView.verticalScrollIndicatorInsets.bottom = viewModel.toolbarHeight
|
||||
})
|
||||
.preference(
|
||||
|
|
|
@ -8,12 +8,16 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
class ComposeViewModel: ObservableObject {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
@Published var authentication: MastodonAuthentication?
|
||||
|
||||
@Published var toolbarHeight: CGFloat = 0
|
||||
@Published var viewDidAppear = false
|
||||
|
||||
@Published var avatarImageURL: URL?
|
||||
@Published var authorName: String = ""
|
||||
|
@ -51,10 +55,38 @@ class ComposeViewModel: ObservableObject {
|
|||
}
|
||||
.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
|
||||
avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
|
||||
authorName = "Alice"
|
||||
authorUsername = "alice"
|
||||
// avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
|
||||
// authorName = "Alice"
|
||||
// authorUsername = "alice"
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -64,11 +96,18 @@ extension ComposeViewModel {
|
|||
func setupAttachmentViewModels(_ viewModels: [StatusAttachmentViewModel]) {
|
||||
attachmentViewModels = viewModels
|
||||
for viewModel in viewModels {
|
||||
// set delegate
|
||||
viewModel.delegate = self
|
||||
// set observed
|
||||
viewModel.objectWillChange.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
.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 Introspect
|
||||
|
||||
struct StatusAttachmentView: View {
|
||||
|
||||
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
|
||||
|
||||
var body: some View {
|
||||
let image = image ?? UIImage.placeholder(color: .systemFill)
|
||||
Color.clear
|
||||
.aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
|
||||
.overlay(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
)
|
||||
.background(Color.gray)
|
||||
.cornerRadius(4)
|
||||
.badgeView(
|
||||
Button(action: {
|
||||
removeButtonAction()
|
||||
}, label: {
|
||||
Image(systemName: "minus.circle.fill")
|
||||
.renderingMode(.original)
|
||||
.font(.system(size: 22, weight: .bold, design: .default))
|
||||
})
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
)
|
||||
ZStack(alignment: .bottom) {
|
||||
if let errorPrompt = errorPrompt {
|
||||
Color.clear
|
||||
.aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
|
||||
.overlay(
|
||||
VStack(alignment: .center) {
|
||||
Image(uiImage: errorPromptImage)
|
||||
Text(errorPrompt)
|
||||
.lineLimit(2)
|
||||
}
|
||||
)
|
||||
.background(Color.gray)
|
||||
} else {
|
||||
Color.clear
|
||||
.aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
|
||||
.overlay(
|
||||
Image(uiImage: image)
|
||||
.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 {
|
||||
static var previews: some View {
|
||||
ScrollView {
|
||||
StatusAttachmentView(
|
||||
image: UIImage(systemName: "photo"),
|
||||
descriptionPlaceholder: "Describe photo",
|
||||
description: .constant(""),
|
||||
errorPrompt: nil,
|
||||
errorPromptImage: StatusAttachmentViewModel.photoFillSplitImage,
|
||||
isUploading: true,
|
||||
progressViewTintColor: .systemFill,
|
||||
removeButtonAction: {
|
||||
// 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 SwiftUI
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
import AVFoundation
|
||||
import GameplayKit
|
||||
import MobileCoreServices
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
protocol StatusAttachmentViewModelDelegate: AnyObject {
|
||||
func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
weak var delegate: StatusAttachmentViewModelDelegate?
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let id = UUID()
|
||||
|
@ -26,15 +39,36 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable {
|
|||
|
||||
// input
|
||||
let file = CurrentValueSubject<Mastodon.Query.MediaAttachment?, Never>(nil)
|
||||
@Published var description = ""
|
||||
let authentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
|
||||
@Published var descriptionContent = ""
|
||||
|
||||
// output
|
||||
let attachment = CurrentValueSubject<Mastodon.Entity.Attachment?, Never>(nil)
|
||||
@Published var thumbnailImage: UIImage?
|
||||
@Published var descriptionPlaceholder = ""
|
||||
@Published var isUploading = true
|
||||
@Published var progressViewTintColor = UIColor.systemFill
|
||||
@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) {
|
||||
self.itemProvider = itemProvider
|
||||
|
||||
// bind attachment from item provider
|
||||
Just(itemProvider)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.flatMap { result -> AnyPublisher<Mastodon.Query.MediaAttachment?, Error> in
|
||||
|
@ -51,18 +85,49 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable {
|
|||
switch completion {
|
||||
case .failure(let error):
|
||||
self.error = error
|
||||
// self.uploadStateMachine.enter(UploadState.Fail.self)
|
||||
self.uploadStateMachine.enter(UploadState.Fail.self)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] file in
|
||||
guard let self = self else { return }
|
||||
self.file.value = file
|
||||
// self.uploadStateMachine.enter(UploadState.Initial.self)
|
||||
self.uploadStateMachine.enter(UploadState.Initial.self)
|
||||
}
|
||||
.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
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map { file -> UIImage? in
|
||||
|
@ -92,6 +157,56 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable {
|
|||
}
|
||||
}
|
||||
.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) {
|
||||
AnimatedImage(imageURL: avatarImageURL)
|
||||
.frame(width: 42, height: 42)
|
||||
.background(Color(UIColor.systemFill))
|
||||
.cornerRadius(4)
|
||||
VStack(alignment: .leading) {
|
||||
Text(name)
|
||||
.font(.headline)
|
||||
Text("@" + username)
|
||||
Text(username)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
|
@ -16,19 +16,22 @@ public struct StatusEditorView: UIViewRepresentable {
|
|||
let width: CGFloat
|
||||
let attributedString: NSAttributedString
|
||||
let keyboardType: UIKeyboardType
|
||||
@Binding var viewDidAppear: Bool
|
||||
|
||||
public init(
|
||||
string: Binding<String>,
|
||||
placeholder: String,
|
||||
width: CGFloat,
|
||||
attributedString: NSAttributedString,
|
||||
keyboardType: UIKeyboardType
|
||||
keyboardType: UIKeyboardType,
|
||||
viewDidAppear: Binding<Bool>
|
||||
) {
|
||||
self._string = string
|
||||
self.placeholder = placeholder
|
||||
self.width = width
|
||||
self.attributedString = attributedString
|
||||
self.keyboardType = keyboardType
|
||||
self._viewDidAppear = viewDidAppear
|
||||
}
|
||||
|
||||
public func makeUIView(context: Context) -> UITextView {
|
||||
|
@ -45,6 +48,7 @@ public struct StatusEditorView: UIViewRepresentable {
|
|||
let widthLayoutConstraint = textView.widthAnchor.constraint(equalToConstant: 100)
|
||||
widthLayoutConstraint.priority = .required - 1
|
||||
context.coordinator.widthLayoutConstraint = widthLayoutConstraint
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
|
@ -55,6 +59,12 @@ public struct StatusEditorView: UIViewRepresentable {
|
|||
|
||||
// update layout
|
||||
context.coordinator.updateLayout(width: width)
|
||||
|
||||
// set becomeFirstResponder
|
||||
if viewDidAppear {
|
||||
viewDidAppear = false
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
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