diff --git a/Localization/app.json b/Localization/app.json index 6275742b..f1024620 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -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." } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ea21a7df..b545bc13 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = ""; }; - 2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; @@ -1204,7 +1214,6 @@ DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = ""; }; DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = ""; }; DBCC3B35261440BA0045B23D /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = ""; }; - DBCC3B88261454BA0045B23D /* CGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImage.swift; sourceTree = ""; }; DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = ""; }; DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Relationship.swift"; sourceTree = ""; }; DBCC3B9A2615849F0045B23D /* PrivateNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateNote.swift; sourceTree = ""; }; @@ -1242,6 +1251,10 @@ DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentView.swift; sourceTree = ""; }; DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentViewModel.swift; sourceTree = ""; }; DBFEF06726A58D07006D7ED1 /* ShareActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareActionExtension.entitlements; sourceTree = ""; }; + DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusAttachmentViewModel+UploadState.swift"; sourceTree = ""; }; + DBFEF07226A6913D006D7ED1 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationBox.swift; sourceTree = ""; }; + DBFEF07A26A6BCE8006D7ED1 /* APIService+Status+Publish.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status+Publish.swift"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 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 = ""; }; @@ -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 = ""; @@ -2791,6 +2805,7 @@ DBC6461926A170AB00B0E31B /* Info.plist */, DBC6461626A170AB00B0E31B /* MainInterface.storyboard */, DBFEF06126A57721006D7ED1 /* Scene */, + DBFEF07426A69140006D7ED1 /* Service */, ); path = ShareActionExtension; sourceTree = ""; @@ -2902,6 +2917,7 @@ DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */, DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */, DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */, + DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */, ); path = View; sourceTree = ""; @@ -2916,6 +2932,14 @@ path = Scene; sourceTree = ""; }; + DBFEF07426A69140006D7ED1 /* Service */ = { + isa = PBXGroup; + children = ( + DBFEF07226A6913D006D7ED1 /* APIService.swift */, + ); + path = Service; + sourceTree = ""; + }; /* 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 */ diff --git a/Mastodon/Extension/CGImage.swift b/Mastodon/Extension/CGImage.swift deleted file mode 100644 index cced4abe..00000000 --- a/Mastodon/Extension/CGImage.swift +++ /dev/null @@ -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 = "" - 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 - - diff --git a/Mastodon/Extension/UIImage.swift b/Mastodon/Extension/UIImage.swift deleted file mode 100644 index 7054661b..00000000 --- a/Mastodon/Extension/UIImage.swift +++ /dev/null @@ -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) - } -} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 90e1af8e..d8b32458 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -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 { diff --git a/Mastodon/Helper/MastodonAuthenticationBox.swift b/Mastodon/Helper/MastodonAuthenticationBox.swift new file mode 100644 index 00000000..71ba50b5 --- /dev/null +++ b/Mastodon/Helper/MastodonAuthenticationBox.swift @@ -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 +} diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index c9546696..bf634b07 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -49,7 +49,7 @@ extension UserProviderFacade { private static func _toggleUserFollowRelationship( context: AppContext, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, + activeMastodonAuthenticationBox: MastodonAuthenticationBox, mastodonUser: AnyPublisher ) -> AnyPublisher, Error> { mastodonUser @@ -111,7 +111,7 @@ extension UserProviderFacade { private static func _toggleUserBlockRelationship( context: AppContext, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, + activeMastodonAuthenticationBox: MastodonAuthenticationBox, mastodonUser: AnyPublisher ) -> AnyPublisher, Error> { mastodonUser @@ -174,7 +174,7 @@ extension UserProviderFacade { private static func _toggleUserMuteRelationship( context: AppContext, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, + activeMastodonAuthenticationBox: MastodonAuthenticationBox, mastodonUser: AnyPublisher ) -> AnyPublisher, Error> { mastodonUser diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 1bf75432..e7369046 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -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"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 1bf75432..e7369046 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -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"; diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 69b9836a..2e059c1a 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -28,7 +28,7 @@ final class ComposeViewModel: NSObject { let isContentWarningComposing = CurrentValueSubject(false) let selectedStatusVisibility: CurrentValueSubject let activeAuthentication: CurrentValueSubject - let activeAuthenticationBox: CurrentValueSubject + let activeAuthenticationBox: CurrentValueSubject let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit let repliedToCellFrame = CurrentValueSubject(.zero) let autoCompleteRetryLayoutTimes = CurrentValueSubject(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 } } } diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift index f8d2bcf3..623ec717 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift @@ -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 { diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 4c3b975c..8102f770 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -26,7 +26,7 @@ final class NotificationViewModel: NSObject { let selectedIndex = CurrentValueSubject(.EveryThing) let noMoreNotification = CurrentValueSubject(false) - let activeMastodonAuthenticationBox: CurrentValueSubject + let activeMastodonAuthenticationBox: CurrentValueSubject let fetchedResultsController: NSFetchedResultsController! let notificationPredicate = CurrentValueSubject(nil) let cellFrameCache = NSCache() diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift index 589ffe19..6b4c1b8c 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift @@ -17,7 +17,7 @@ final class FavoriteViewModel { // input let context: AppContext - let activeMastodonAuthenticationBox: CurrentValueSubject + let activeMastodonAuthenticationBox: CurrentValueSubject let statusFetchedResultsController: StatusFetchedResultsController let cellFrameCache = NSCache() diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 5e8874c2..05db1a88 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -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) diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift index df95cb00..178fc18a 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Data.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -17,7 +17,7 @@ extension ReportViewModel { func requestRecentStatus( domain: String, accountId: String, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) { context.apiService.userTimeline( domain: domain, diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index 8631963c..c8e59e8d 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -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 diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index 681fa0f5..4929ccca 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -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) diff --git a/Mastodon/Service/APIService/APIService+Block.swift b/Mastodon/Service/APIService/APIService+Block.swift index c28db9da..209ee361 100644 --- a/Mastodon/Service/APIService/APIService+Block.swift +++ b/Mastodon/Service/APIService/APIService+Block.swift @@ -16,7 +16,7 @@ extension APIService { func toggleBlock( for mastodonUser: MastodonUser, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + activeMastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+DomainBlock.swift b/Mastodon/Service/APIService/APIService+DomainBlock.swift index 887c3f07..222e1299 100644 --- a/Mastodon/Service/APIService/APIService+DomainBlock.swift +++ b/Mastodon/Service/APIService/APIService+DomainBlock.swift @@ -17,7 +17,7 @@ extension APIService { func getDomainblocks( domain: String, limit: Int = onceRequestDomainBlocksMaxCount, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization @@ -71,7 +71,7 @@ extension APIService { func blockDomain( user: MastodonUser, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization @@ -105,7 +105,7 @@ extension APIService { func unblockDomain( user: MastodonUser, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index d3f1d81e..78a20d10 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -61,7 +61,7 @@ extension APIService { func favorite( statusID: Mastodon.Entity.Status.ID, favoriteKind: Mastodon.API.Favorites.FavoriteKind, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let requestMastodonUserID = mastodonAuthenticationBox.userID diff --git a/Mastodon/Service/APIService/APIService+Filter.swift b/Mastodon/Service/APIService/APIService+Filter.swift index 5ecd1077..01f8087a 100644 --- a/Mastodon/Service/APIService/APIService+Filter.swift +++ b/Mastodon/Service/APIService/APIService+Filter.swift @@ -15,7 +15,7 @@ import MastodonSDK extension APIService { func filters( - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let domain = mastodonAuthenticationBox.domain diff --git a/Mastodon/Service/APIService/APIService+Follow.swift b/Mastodon/Service/APIService/APIService+Follow.swift index f527878f..ac2ccbea 100644 --- a/Mastodon/Service/APIService/APIService+Follow.swift +++ b/Mastodon/Service/APIService/APIService+Follow.swift @@ -24,7 +24,7 @@ extension APIService { /// - Returns: publisher for `Relationship` func toggleFollow( for mastodonUser: MastodonUser, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + activeMastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+FollowRequest.swift b/Mastodon/Service/APIService/APIService+FollowRequest.swift index c40fcad5..0f5c3c25 100644 --- a/Mastodon/Service/APIService/APIService+FollowRequest.swift +++ b/Mastodon/Service/APIService/APIService+FollowRequest.swift @@ -17,7 +17,7 @@ import MastodonSDK extension APIService { func acceptFollowRequest( mastodonUserID: MastodonUser.ID, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+HashtagTimeline.swift b/Mastodon/Service/APIService/APIService+HashtagTimeline.swift index 69c2c748..241c7885 100644 --- a/Mastodon/Service/APIService/APIService+HashtagTimeline.swift +++ b/Mastodon/Service/APIService/APIService+HashtagTimeline.swift @@ -22,7 +22,7 @@ extension APIService { limit: Int = onceRequestStatusMaxCount, local: Bool? = nil, hashtag: String, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization let requestMastodonUserID = authorizationBox.userID diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/Mastodon/Service/APIService/APIService+HomeTimeline.swift index d4cbe69c..28f68274 100644 --- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift +++ b/Mastodon/Service/APIService/APIService+HomeTimeline.swift @@ -21,7 +21,7 @@ extension APIService { maxID: Mastodon.Entity.Status.ID? = nil, limit: Int = onceRequestStatusMaxCount, local: Bool? = nil, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization let requestMastodonUserID = authorizationBox.userID diff --git a/Mastodon/Service/APIService/APIService+Media.swift b/Mastodon/Service/APIService/APIService+Media.swift index 7c1fd64d..d6b1d6c2 100644 --- a/Mastodon/Service/APIService/APIService+Media.swift +++ b/Mastodon/Service/APIService/APIService+Media.swift @@ -14,7 +14,7 @@ extension APIService { func uploadMedia( domain: String, query: Mastodon.API.Media.UploadMediaQuery, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, + mastodonAuthenticationBox: MastodonAuthenticationBox, needsFallback: Bool ) -> AnyPublisher, 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, 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, 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, Error> { let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Mute.swift b/Mastodon/Service/APIService/APIService+Mute.swift index 9d992ab6..40f97acd 100644 --- a/Mastodon/Service/APIService/APIService+Mute.swift +++ b/Mastodon/Service/APIService/APIService+Mute.swift @@ -16,7 +16,7 @@ extension APIService { func toggleMute( for mastodonUser: MastodonUser, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + activeMastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index dfd87bc1..9f7d3bb5 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -16,7 +16,7 @@ extension APIService { func allNotifications( domain: String, query: Mastodon.API.Notifications.Query, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Poll.swift b/Mastodon/Service/APIService/APIService+Poll.swift index 0b240466..ca091161 100644 --- a/Mastodon/Service/APIService/APIService+Poll.swift +++ b/Mastodon/Service/APIService/APIService+Poll.swift @@ -19,7 +19,7 @@ extension APIService { domain: String, pollID: Mastodon.Entity.Poll.ID, pollObjectID: NSManagedObjectID, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let requestMastodonUserID = mastodonAuthenticationBox.userID diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index adfac306..fd020614 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -62,7 +62,7 @@ extension APIService { func reblog( statusID: Mastodon.Entity.Status.ID, reblogKind: Mastodon.API.Reblog.ReblogKind, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let domain = mastodonAuthenticationBox.domain let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/Mastodon/Service/APIService/APIService+Recommend.swift index a3bcb3e3..458cb740 100644 --- a/Mastodon/Service/APIService/APIService+Recommend.swift +++ b/Mastodon/Service/APIService/APIService+Recommend.swift @@ -16,7 +16,7 @@ extension APIService { func suggestionAccount( domain: String, query: Mastodon.API.Suggestions.Query?, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, 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, Error> { let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Relationship.swift b/Mastodon/Service/APIService/APIService+Relationship.swift index b0ef2926..7efd2b39 100644 --- a/Mastodon/Service/APIService/APIService+Relationship.swift +++ b/Mastodon/Service/APIService/APIService+Relationship.swift @@ -17,7 +17,7 @@ extension APIService { func relationship( domain: String, accountIDs: [Mastodon.Entity.Account.ID], - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization let requestMastodonUserID = authorizationBox.userID diff --git a/Mastodon/Service/APIService/APIService+Report.swift b/Mastodon/Service/APIService/APIService+Report.swift index 3c170c62..531c7218 100644 --- a/Mastodon/Service/APIService/APIService+Report.swift +++ b/Mastodon/Service/APIService/APIService+Report.swift @@ -14,7 +14,7 @@ extension APIService { func report( domain: String, query: Mastodon.API.Reports.FileReportQuery, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization diff --git a/Mastodon/Service/APIService/APIService+Search.swift b/Mastodon/Service/APIService/APIService+Search.swift index e1642fb5..4b636806 100644 --- a/Mastodon/Service/APIService/APIService+Search.swift +++ b/Mastodon/Service/APIService/APIService+Search.swift @@ -15,7 +15,7 @@ extension APIService { func search( domain: String, query: Mastodon.API.V2.Search.Query, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let requestMastodonUserID = mastodonAuthenticationBox.userID diff --git a/Mastodon/Service/APIService/APIService+Status+Publish.swift b/Mastodon/Service/APIService/APIService+Status+Publish.swift new file mode 100644 index 00000000..45964602 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Status+Publish.swift @@ -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, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + + return Mastodon.API.Statuses.publishStatus( + session: session, + domain: domain, + query: query, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, 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 in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + #endif + } + .eraseToAnyPublisher() + } + +} diff --git a/Mastodon/Service/APIService/APIService+Status.swift b/Mastodon/Service/APIService/APIService+Status.swift index 01bc667e..7f82406f 100644 --- a/Mastodon/Service/APIService/APIService+Status.swift +++ b/Mastodon/Service/APIService/APIService+Status.swift @@ -14,48 +14,11 @@ import DateToolsSwift import MastodonSDK extension APIService { - - func publishStatus( - domain: String, - query: Mastodon.API.Statuses.PublishStatusQuery, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox - ) -> AnyPublisher, Error> { - let authorization = mastodonAuthenticationBox.userAuthorization - - return Mastodon.API.Statuses.publishStatus( - session: session, - domain: domain, - query: query, - authorization: authorization - ) - .flatMap { response -> AnyPublisher, 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 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, 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, Error> { let authorization = authorizationBox.userAuthorization let query = Mastodon.API.Statuses.DeleteStatusQuery(id: statusID) diff --git a/Mastodon/Service/APIService/APIService+Subscriptions.swift b/Mastodon/Service/APIService/APIService+Subscriptions.swift index ceaff45f..e9df2bc5 100644 --- a/Mastodon/Service/APIService/APIService+Subscriptions.swift +++ b/Mastodon/Service/APIService/APIService+Subscriptions.swift @@ -17,7 +17,7 @@ extension APIService { func createSubscription( subscriptionObjectID: NSManagedObjectID, query: Mastodon.API.Subscriptions.CreateSubscriptionQuery, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let domain = mastodonAuthenticationBox.domain @@ -50,7 +50,7 @@ extension APIService { } func cancelSubscription( - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let domain = mastodonAuthenticationBox.domain diff --git a/Mastodon/Service/APIService/APIService+Thread.swift b/Mastodon/Service/APIService/APIService+Thread.swift index 2633518c..3bebdffe 100644 --- a/Mastodon/Service/APIService/APIService+Thread.swift +++ b/Mastodon/Service/APIService/APIService+Thread.swift @@ -17,7 +17,7 @@ extension APIService { func statusContext( domain: String, statusID: Mastodon.Entity.Status.ID, - mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization guard domain == mastodonAuthenticationBox.domain else { diff --git a/Mastodon/Service/APIService/APIService+UserTimeline.swift b/Mastodon/Service/APIService/APIService+UserTimeline.swift index 0e1e223b..7a449d37 100644 --- a/Mastodon/Service/APIService/APIService+UserTimeline.swift +++ b/Mastodon/Service/APIService/APIService+UserTimeline.swift @@ -23,7 +23,7 @@ extension APIService { excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil, onlyMedia: Bool? = nil, - authorizationBox: AuthenticationService.MastodonAuthenticationBox + authorizationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = authorizationBox.userAuthorization let requestMastodonUserID = authorizationBox.userID diff --git a/Mastodon/Service/APIService/APIService.swift b/Mastodon/Service/APIService/APIService.swift index 1316f2cf..9d1468ce 100644 --- a/Mastodon/Service/APIService/APIService.swift +++ b/Mastodon/Service/APIService/APIService.swift @@ -21,7 +21,6 @@ final class APIService { // internal let session: URLSession - // input let backgroundManagedObjectContext: NSManagedObjectContext diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift index a0bbca57..f6ece044 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/Mastodon/Service/AuthenticationService.swift @@ -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(nil) - let activeMastodonAuthenticationBox = CurrentValueSubject(nil) + let activeMastodonAuthenticationBox = CurrentValueSubject(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, 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), diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index 7976156f..8474ac4d 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -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) diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index f2841b26..2b08b0db 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -27,7 +27,7 @@ final class MastodonAttachmentService { // input let context: AppContext - var authenticationBox: AuthenticationService.MastodonAuthenticationBox? + var authenticationBox: MastodonAuthenticationBox? let file = CurrentValueSubject(nil) let description = CurrentValueSubject(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) } diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift index 76e9b522..ffe4c991 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/Mastodon/Service/NotificationService.swift @@ -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() diff --git a/Mastodon/Service/SettingService.swift b/Mastodon/Service/SettingService.swift index 7da8c368..37246765 100644 --- a/Mastodon/Service/SettingService.swift +++ b/Mastodon/Service/SettingService.swift @@ -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 } diff --git a/Mastodon/Service/StatusPrefetchingService.swift b/Mastodon/Service/StatusPrefetchingService.swift index 34b1d7a0..e22ba69f 100644 --- a/Mastodon/Service/StatusPrefetchingService.swift +++ b/Mastodon/Service/StatusPrefetchingService.swift @@ -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 } diff --git a/MastodonSDK/Sources/MastodonExtension/CGImage.swift b/MastodonSDK/Sources/MastodonExtension/CGImage.swift new file mode 100644 index 00000000..41f2de0b --- /dev/null +++ b/MastodonSDK/Sources/MastodonExtension/CGImage.swift @@ -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 + } +} diff --git a/MastodonSDK/Sources/MastodonExtension/UIImage.swift b/MastodonSDK/Sources/MastodonExtension/UIImage.swift index 79896fda..178d289d 100644 --- a/MastodonSDK/Sources/MastodonExtension/UIImage.swift +++ b/MastodonSDK/Sources/MastodonExtension/UIImage.swift @@ -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) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift b/MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift index 2524fcb1..6662f90e 100644 --- a/MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift +++ b/MastodonSDK/Sources/MastodonUI/Vendor/ItemProviderLoader.swift @@ -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 { diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 3fba5adb..a3cef66d 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -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) } } diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift index 777e5be5..060181ae 100644 --- a/ShareActionExtension/Scene/ShareViewModel.swift +++ b/ShareActionExtension/Scene/ShareViewModel.swift @@ -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?, Never>(nil) let isFetchAuthentication = CurrentValueSubject(true) + let isPublishing = CurrentValueSubject(false) let isBusy = CurrentValueSubject(true) let isValid = CurrentValueSubject(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, 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, Error>] = { + var subscriptions: [AnyPublisher, 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, 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() + } +} diff --git a/ShareActionExtension/Scene/View/ComposeView.swift b/ShareActionExtension/Scene/View/ComposeView.swift index 65b30913..f65e35b4 100644 --- a/ShareActionExtension/Scene/View/ComposeView.swift +++ b/ShareActionExtension/Scene/View/ComposeView.swift @@ -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( diff --git a/ShareActionExtension/Scene/View/ComposeViewModel.swift b/ShareActionExtension/Scene/View/ComposeViewModel.swift index 06db193d..8d60481d 100644 --- a/ShareActionExtension/Scene/View/ComposeViewModel.swift +++ b/ShareActionExtension/Scene/View/ComposeViewModel.swift @@ -8,12 +8,16 @@ import Foundation import SwiftUI import Combine +import CoreDataStack class ComposeViewModel: ObservableObject { var disposeBag = Set() + @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 + } + } +} diff --git a/ShareActionExtension/Scene/View/StatusAttachmentView.swift b/ShareActionExtension/Scene/View/StatusAttachmentView.swift index 0f52afeb..4bc2ff9a 100644 --- a/ShareActionExtension/Scene/View/StatusAttachmentView.swift +++ b/ShareActionExtension/Scene/View/StatusAttachmentView.swift @@ -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( + 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 } diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift new file mode 100644 index 00000000..cfd0a4de --- /dev/null +++ b/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift @@ -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 ?? "")") + 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 + } + } + +} + diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift index ba6a4d51..f0c1e644 100644 --- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift +++ b/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift @@ -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() let id = UUID() @@ -26,15 +39,36 @@ final class StatusAttachmentViewModel: ObservableObject, Identifiable { // input let file = CurrentValueSubject(nil) - @Published var description = "" + let authentication = CurrentValueSubject(nil) + @Published var descriptionContent = "" // output + let attachment = CurrentValueSubject(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(nil) init(itemProvider: NSItemProvider) { self.itemProvider = itemProvider + // bind attachment from item provider Just(itemProvider) .receive(on: DispatchQueue.main) .flatMap { result -> AnyPublisher 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) } } diff --git a/ShareActionExtension/Scene/View/StatusAuthorView.swift b/ShareActionExtension/Scene/View/StatusAuthorView.swift index c729bb79..189a7adc 100644 --- a/ShareActionExtension/Scene/View/StatusAuthorView.swift +++ b/ShareActionExtension/Scene/View/StatusAuthorView.swift @@ -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) } diff --git a/ShareActionExtension/Scene/View/StatusEditorView.swift b/ShareActionExtension/Scene/View/StatusEditorView.swift index e6944883..79fedc00 100644 --- a/ShareActionExtension/Scene/View/StatusEditorView.swift +++ b/ShareActionExtension/Scene/View/StatusEditorView.swift @@ -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, placeholder: String, width: CGFloat, attributedString: NSAttributedString, - keyboardType: UIKeyboardType + keyboardType: UIKeyboardType, + viewDidAppear: Binding ) { 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 { diff --git a/ShareActionExtension/Service/APIService.swift b/ShareActionExtension/Service/APIService.swift new file mode 100644 index 00000000..a8112167 --- /dev/null +++ b/ShareActionExtension/Service/APIService.swift @@ -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() + + static let shared = APIService() + + // internal + let session: URLSession + + // output + let error = PassthroughSubject() + + private init() { + self.session = URLSession(configuration: .default) + } + +}