From a9744146cefaf57e4b8ad0c672a33936fee5d9ca Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:42:49 +0800 Subject: [PATCH] feat: add video attachment post supports --- Localization/app.json | 6 +- Mastodon.xcodeproj/project.pbxproj | 236 +++++++++--------- .../xcschemes/xcschememanagement.plist | 4 +- .../Section/ComposeStatusSection.swift | 25 +- Mastodon/Generated/Strings.swift | 6 + .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Scene/Compose/ComposeViewController.swift | 38 ++- Mastodon/Scene/Compose/ComposeViewModel.swift | 55 ++++ ...tachmentContainerView+EmptyStateView.swift | 2 + ...meTimelineViewController+DebugAction.swift | 10 + .../Header/ProfileHeaderViewController.swift | 4 +- ...astodonAttachmentService+UploadState.swift | 12 +- .../MastodonAttachmentService.swift | 69 ++++- Mastodon/Vender/PHPickerResultLoader.swift | 35 ++- .../MastodonSDK/API/Mastodon+API+Media.swift | 78 +++++- .../Sources/MastodonSDK/Extension/Data.swift | 7 +- .../MastodonSDK/Query/MediaAttachment.swift | 19 +- .../Query/MultipartFormValue.swift | 5 + .../MastodonSDK/Query/SerialStream.swift | 147 +++++++++++ Podfile | 17 +- Podfile.lock | 6 +- 22 files changed, 606 insertions(+), 179 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift diff --git a/Localization/app.json b/Localization/app.json index 920ec0d27..69d513600 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -21,7 +21,11 @@ }, "publish_post_failure": { "title": "Publish Failure", - "message": "Failed to publish the post.\nPlease check your internet connection." + "message": "Failed to publish the post.\nPlease check your internet connection.", + "attchments_message": { + "video_attach_with_photo": "Cannot attach a video to a status that already contains images.", + "more_than_one_video": "Cannot attach more than one video." + } }, "sign_out": { "title": "Sign out", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 28f113616..b1aaa14b4 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -80,7 +80,7 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; + 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */; }; 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; @@ -90,7 +90,7 @@ 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; @@ -116,7 +116,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; + 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB968263A833E007C1D71 /* DomainBlock.swift */; }; @@ -164,7 +164,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; @@ -178,14 +178,13 @@ 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; - 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; - D86919F5080C3F228CCD17D1 /* Pods_Mastodon_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */; }; - DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; + B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; + DB00CA972632DDB600A54956 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -217,7 +216,7 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; }; DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; }; DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; }; - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; @@ -258,7 +257,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; + DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -294,7 +293,7 @@ DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; - DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; + DB6805102637D0F800430867 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* SwiftPackageProductDependency */; }; DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; @@ -307,7 +306,7 @@ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; - DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; }; + DB6D9F42263527CE008423CD /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */; }; DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; }; DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; }; DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; }; @@ -318,8 +317,8 @@ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; }; - DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; }; - DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; }; + DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; @@ -383,7 +382,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; + DB9A487E2603456B008B817C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* SwiftPackageProductDependency */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; }; @@ -417,7 +416,7 @@ DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; + DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; @@ -464,10 +463,11 @@ DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; + DBF8AE862632992800C9C23C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; + EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -552,7 +552,7 @@ files = ( DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, - DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */, + DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -704,10 +704,10 @@ 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = ""; }; 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; + 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; - 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = ""; }; 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; @@ -741,9 +741,11 @@ 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; - 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; + 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.debug.xcconfig"; sourceTree = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.release.xcconfig"; sourceTree = ""; }; B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = ""; }; @@ -1036,6 +1038,8 @@ DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewHeaderFooterView.swift; sourceTree = ""; }; DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; 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 = ""; }; + ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.release.xcconfig"; sourceTree = ""; }; + F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1043,20 +1047,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */, - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, + DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */, + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, - DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, + DB9A487E2603456B008B817C /* BuildFile in Frameworks */, + 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */, + DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */, + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, + DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, + 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, ); @@ -1083,8 +1087,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6805102637D0F800430867 /* KeychainAccess in Frameworks */, - D86919F5080C3F228CCD17D1 /* Pods_Mastodon_AppShared.framework in Frameworks */, + DB6805102637D0F800430867 /* BuildFile in Frameworks */, + EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1108,11 +1112,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */, - DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */, - DBF8AE862632992800C9C23C /* Base85 in Frameworks */, + DB00CA972632DDB600A54956 /* BuildFile in Frameworks */, + DB6D9F42263527CE008423CD /* BuildFile in Frameworks */, + DBF8AE862632992800C9C23C /* BuildFile in Frameworks */, DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */, - 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */, + B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1199,6 +1203,10 @@ B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */, D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */, B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */, + 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */, + ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */, + 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */, + 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1544,8 +1552,8 @@ A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */, 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */, 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */, - 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */, - 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */, + F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */, + 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */, ); name = Frameworks; sourceTree = ""; @@ -2489,17 +2497,17 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* AlamofireImage */, - 5D526FE125BE9AC400460CB9 /* MastodonSDK */, - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, - 2D42FF6025C8177C004A627A /* ActiveLabel */, - DB0140BC25C40D7500F9F3CF /* CommonOSLog */, - DB5086B725CC0D6400C2C187 /* Kingfisher */, - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, - 2D939AC725EE14620076FA61 /* CropViewController */, - DB9A487D2603456B008B817C /* UITextView+Placeholder */, - DBB525072611EAC0002F1F29 /* Tabman */, - DB6F5E31264E7410009108F4 /* TwitterTextEditor */, + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, + DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */, + 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */, + 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */, + DB9A487D2603456B008B817C /* SwiftPackageProductDependency */, + DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */, + DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2560,7 +2568,7 @@ ); name = AppShared; packageProductDependencies = ( - DB68050F2637D0F800430867 /* KeychainAccess */, + DB68050F2637D0F800430867 /* SwiftPackageProductDependency */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -2620,9 +2628,9 @@ ); name = NotificationService; packageProductDependencies = ( - DBF8AE852632992800C9C23C /* Base85 */, - DB00CA962632DDB600A54956 /* CommonOSLog */, - DB6D9F41263527CE008423CD /* AlamofireImage */, + DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */, + DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */, + DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -2677,18 +2685,18 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, - DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, - DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, - DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, + DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */, + 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */, + 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */, + DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */, + DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */, + DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */, + DB6804722637CC1200430867 /* RemoteSwiftPackageReference */, + DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -2780,7 +2788,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mastodon-NotificationService-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2863,7 +2871,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mastodon-AppShared-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-AppShared-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -3746,7 +3754,7 @@ }; DB6804892637CD4C00430867 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */; + baseConfigurationReference = 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; @@ -3777,7 +3785,7 @@ }; DB68048A2637CD4C00430867 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */; + baseConfigurationReference = ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; @@ -3904,7 +3912,7 @@ }; DBF8AE1C263293E400C9C23C /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */; + baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; @@ -3927,7 +3935,7 @@ }; DBF8AE1D263293E400C9C23C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */; + baseConfigurationReference = 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; @@ -4026,7 +4034,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -4034,7 +4042,7 @@ version = 5.0.2; }; }; - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { + 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; requirement = { @@ -4042,7 +4050,7 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -4050,7 +4058,7 @@ minimumVersion = 3.1.0; }; }; - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { + 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; requirement = { @@ -4058,7 +4066,7 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -4066,7 +4074,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -4074,7 +4082,7 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -4082,7 +4090,7 @@ minimumVersion = 6.1.0; }; }; - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + DB6804722637CC1200430867 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4090,7 +4098,7 @@ minimumVersion = 4.2.2; }; }; - DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { + DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git"; requirement = { @@ -4098,7 +4106,7 @@ kind = branch; }; }; - DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { + DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; requirement = { @@ -4106,7 +4114,7 @@ minimumVersion = 1.4.1; }; }; - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { + DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; requirement = { @@ -4114,7 +4122,7 @@ minimumVersion = 2.11.0; }; }; - DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = { + DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/Base85.git"; requirement = { @@ -4125,78 +4133,78 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* ActiveLabel */ = { + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; + package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; productName = ActiveLabel; }; - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { + 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; + package = 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; + package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; productName = AlamofireNetworkActivityIndicator; }; - 2D939AC725EE14620076FA61 /* CropViewController */ = { + 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; + package = 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */; productName = CropViewController; }; - 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB00CA962632DDB600A54956 /* CommonOSLog */ = { + DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* Kingfisher */ = { + DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; + package = DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */; productName = Kingfisher; }; - DB68050F2637D0F800430867 /* KeychainAccess */ = { + DB68050F2637D0F800430867 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; + package = DB6804722637CC1200430867 /* RemoteSwiftPackageReference */; productName = KeychainAccess; }; - DB6D9F41263527CE008423CD /* AlamofireImage */ = { + DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; - DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = { + DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; + package = DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */; productName = TwitterTextEditor; }; - DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { + DB9A487D2603456B008B817C /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; + package = DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */; productName = "UITextView+Placeholder"; }; - DBB525072611EAC0002F1F29 /* Tabman */ = { + DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; + package = DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */; productName = Tabman; }; - DBF8AE852632992800C9C23C /* Base85 */ = { + DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */; + package = DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */; productName = Base85; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 326857269..217fe1993 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 14 + 15 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 15 + 16 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 46d00dbef..a1e6170e8 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -149,13 +149,12 @@ extension ComposeStatusSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value cell.delegate = composeStatusAttachmentTableViewCellDelegate - attachmentService.data + attachmentService.thumbnailImage .receive(on: DispatchQueue.main) - .sink { [weak cell] imageData in + .sink { [weak cell] thumbnailImage in guard let cell = cell else { return } let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) - guard let imageData = imageData, - let image = UIImage(data: imageData) else { + guard let image = thumbnailImage else { let placeholder = UIImage.placeholder( size: size, color: Asset.Colors.Background.systemGroupedBackground.color @@ -176,18 +175,32 @@ extension ComposeStatusSection { attachmentService.error.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { [weak cell] uploadState, error in + .sink { [weak cell, weak attachmentService] uploadState, error in guard let cell = cell else { return } + guard let attachmentService = attachmentService else { return } cell.attachmentContainerView.emptyStateView.isHidden = error == nil cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil - if let _ = error { + if let error = error { cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription } else { guard let uploadState = uploadState else { return } switch uploadState { case is MastodonAttachmentService.UploadState.Finish, is MastodonAttachmentService.UploadState.Fail: cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = { + if let file = attachmentService.file.value { + 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) + } + } else { + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + } + }() default: break } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index de8403ac0..5790f7134 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -44,6 +44,12 @@ 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 { + /// Cannot attach more than one video. + internal static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo") + /// Cannot attach a video to a status that already contains images. + internal static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto") + } } internal enum SavePhotoFailure { /// Please enable photo libaray access permission to save photo. diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 80fce62fa..81bf772e1 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -6,6 +6,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status 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 80fce62fa..81bf772e1 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -6,6 +6,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status 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/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 80a64064b..2e3e5fe7c 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -75,12 +75,15 @@ final class ComposeViewController: UIViewController, NeedsDependency { var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! let composeToolbarBackgroundView = UIView() - private(set) lazy var imagePicker: PHPickerViewController = { + static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { var configuration = PHPickerConfiguration() - configuration.filter = .images - configuration.selectionLimit = 4 - - let imagePicker = PHPickerViewController(configuration: configuration) + configuration.filter = .any(of: [.images, .videos]) + configuration.selectionLimit = selectionLimit + return configuration + } + + private(set) lazy var photoLibraryPicker: PHPickerViewController = { + let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) imagePicker.delegate = self return imagePicker }() @@ -567,12 +570,9 @@ extension ComposeViewController { } private func resetImagePicker() { - var configuration = PHPickerConfiguration() - configuration.filter = .images let selectionLimit = max(1, 4 - viewModel.attachmentServices.value.count) - configuration.selectionLimit = selectionLimit - - imagePicker = createImagePicker(configuration: configuration) + let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) + photoLibraryPicker = createImagePicker(configuration: configuration) } private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { @@ -610,6 +610,16 @@ extension ComposeViewController { @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + do { + try viewModel.checkAttachmentPrecondition() + } catch { + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + return + } + guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { // TODO: handle error return @@ -913,7 +923,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate { func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { switch type { case .photoLibrary: - present(imagePicker, animated: true, completion: nil) + present(photoLibraryPicker, animated: true, completion: nil) case .camera: present(imagePickerController, animated: true, completion: nil) case .browse: @@ -1120,8 +1130,12 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega var attachmentServices = viewModel.attachmentServices.value guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } + let removedItem = attachmentServices[index] attachmentServices.remove(at: index) viewModel.attachmentServices.value = attachmentServices + + // cancel task + removedItem.disposeBag.removeAll() } } @@ -1365,7 +1379,7 @@ extension ComposeViewController { case .mediaBrowse: present(documentPickerController, animated: true, completion: nil) case .mediaPhotoLibrary: - present(imagePicker, animated: true, completion: nil) + present(photoLibraryPicker, animated: true, completion: nil) case .mediaCamera: guard UIImagePickerController.isSourceTypeAvailable(.camera) else { return diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 097dd9fdd..0ac124586 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -291,6 +291,8 @@ final class ComposeViewModel { ) .receive(on: DispatchQueue.main) .sink { [weak self] attachmentServices, isPollComposing, pollAttributes in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: trigger attachments upload…", ((#file as NSString).lastPathComponent), #line, #function) + guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() @@ -405,6 +407,59 @@ extension ComposeViewModel { } } +extension ComposeViewModel { + + enum AttachmentPrecondition: Error, LocalizedError { + case videoAttachWithPhoto + case moreThanOneVideo + + var errorDescription: String? { + return L10n.Common.Alerts.PublishPostFailure.title + } + + var failureReason: String? { + switch self { + case .videoAttachWithPhoto: + return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.videoAttachWithPhoto + case .moreThanOneVideo: + return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.moreThanOneVideo + } + } + } + + // check exclusive limit: + // - up to 1 video + // - up to 4 photos + func checkAttachmentPrecondition() throws { + let attachmentServices = self.attachmentServices.value + guard !attachmentServices.isEmpty else { return } + var photoAttachmentServices: [MastodonAttachmentService] = [] + var videoAttachmentServices: [MastodonAttachmentService] = [] + attachmentServices.forEach { service in + guard let file = service.file.value else { + assertionFailure() + return + } + switch file { + case .jpeg, .png, .gif: + photoAttachmentServices.append(service) + case .other: + videoAttachmentServices.append(service) + } + } + + if !videoAttachmentServices.isEmpty { + guard videoAttachmentServices.count == 1 else { + throw AttachmentPrecondition.moreThanOneVideo + } + guard photoAttachmentServices.isEmpty else { + throw AttachmentPrecondition.videoAttachWithPhoto + } + } + } + +} + // MARK: - MastodonAttachmentServiceDelegate extension ComposeViewModel: MastodonAttachmentServiceDelegate { func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift index 353fe7497..ca45e33d5 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -29,6 +29,8 @@ extension AttachmentContainerView { label.textAlignment = .center label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) label.numberOfLines = 2 + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.3 return label }() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 152bb62f0..69eff4a82 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -11,6 +11,8 @@ import CoreData import CoreDataStack #if DEBUG +import FLEX + extension HomeTimelineViewController { var debugMenu: UIMenu { let menu = UIMenu( @@ -19,6 +21,10 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ + UIAction(title: "Show FLEX", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.showFLEXAction(action) + }), moveMenu, dropMenu, UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in @@ -115,6 +121,10 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { + @objc private func showFLEXAction(_ sender: UIAction) { + FLEXManager.shared.showExplorer() + } + @objc private func moveToTopGapAction(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index c3495b7f1..fbc8be624 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -448,9 +448,9 @@ extension ProfileHeaderViewController: PHPickerViewControllerDelegate { case .finished: break } - } receiveValue: { [weak self] imageData in + } receiveValue: { [weak self] file in guard let self = self else { return } - guard let imageData = imageData else { return } + guard let imageData = file?.data else { return } guard let image = UIImage(data: imageData) else { return } self.cropImage(image: image, pickerViewController: picker) } diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index 9fd4b1298..23233ec3f 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -35,7 +35,7 @@ extension MastodonAttachmentService.UploadState { return true } - if service?.imageData.value != nil { + if service?.file.value != nil { return stateClass == Uploading.self } else { return stateClass == Fail.self @@ -53,15 +53,8 @@ extension MastodonAttachmentService.UploadState { guard let service = service, let stateMachine = stateMachine else { return } guard let authenticationBox = service.authenticationBox else { return } - guard let imageData = service.imageData.value else { return } + guard let file = service.file.value else { return } - let file: Mastodon.Query.MediaAttachment = { - if imageData.kf.imageFormat == .PNG { - return .png(imageData) - } else { - return .jpeg(imageData) - } - }() let description = service.description.value let query = Mastodon.API.Media.UploadMeidaQuery( file: file, @@ -81,6 +74,7 @@ extension MastodonAttachmentService.UploadState { case .failure(let error): 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) + stateMachine.enter(Fail.self) case .finished: os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment success", ((#file as NSString).lastPathComponent), #line, #function) break diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index fd95d2634..4b843ef40 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -5,11 +5,13 @@ // Created by MainasuK Cirno on 2021-3-17. // +import os.log import UIKit import Combine import PhotosUI import Kingfisher import GameplayKit +import MobileCoreServices import MastodonSDK protocol MastodonAttachmentServiceDelegate: AnyObject { @@ -26,12 +28,12 @@ final class MastodonAttachmentService { // input let context: AppContext var authenticationBox: AuthenticationService.MastodonAuthenticationBox? + let file = CurrentValueSubject(nil) + let description = CurrentValueSubject(nil) // output - // TODO: handle video/GIF/Audio data - let imageData = CurrentValueSubject(nil) + let thumbnailImage = CurrentValueSubject(nil) let attachment = CurrentValueSubject(nil) - let description = CurrentValueSubject(nil) let error = CurrentValueSubject(nil) private(set) lazy var uploadStateMachine: GKStateMachine = { @@ -58,7 +60,16 @@ final class MastodonAttachmentService { setupServiceObserver() - PHPickerResultLoader.loadImageData(from: pickerResult) + Just(pickerResult) + .flatMap { result -> AnyPublisher in + if result.itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) { + return PHPickerResultLoader.loadImageData(from: result).eraseToAnyPublisher() + } + if result.itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) { + return PHPickerResultLoader.loadVideoData(from: result).eraseToAnyPublisher() + } + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } .sink { [weak self] completion in guard let self = self else { return } switch completion { @@ -68,12 +79,42 @@ final class MastodonAttachmentService { case .finished: break } - } receiveValue: { [weak self] imageData in + } receiveValue: { [weak self] file in guard let self = self else { return } - self.imageData.value = imageData + self.file.value = file self.uploadStateMachine.enter(UploadState.Initial.self) } .store(in: &disposeBag) + + file + .map { file -> UIImage? in + guard let file = file else { + return nil + } + + switch file { + case .jpeg(let data), .png(let data): + return data.flatMap { UIImage(data: $0) } + case .gif: + // TODO: + return nil + case .other(let url, _, _): + guard let url = url, FileManager.default.fileExists(atPath: url.path) else { return nil } + let asset = AVURLAsset(url: url) + let assetImageGenerator = AVAssetImageGenerator(asset: asset) + assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation + do { + let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) + let image = UIImage(cgImage: cgImage) + return image + } catch { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: thumbnail generate fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + return nil + } + } + } + .assign(to: \.value, on: thumbnailImage) + .store(in: &disposeBag) } init( @@ -87,7 +128,7 @@ final class MastodonAttachmentService { setupServiceObserver() - imageData.value = image.jpegData(compressionQuality: 0.75) + file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) uploadStateMachine.enter(UploadState.Initial.self) } @@ -102,7 +143,7 @@ final class MastodonAttachmentService { setupServiceObserver() - self.imageData.value = imageData + self.file.value = .jpeg(imageData) uploadStateMachine.enter(UploadState.Initial.self) } @@ -115,6 +156,18 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension MastodonAttachmentService { + enum AttachmentError: Error { + case invalidAttachmentType + case attachmentTooLarge + } + } extension MastodonAttachmentService { diff --git a/Mastodon/Vender/PHPickerResultLoader.swift b/Mastodon/Vender/PHPickerResultLoader.swift index 7e083001c..88a9a7554 100644 --- a/Mastodon/Vender/PHPickerResultLoader.swift +++ b/Mastodon/Vender/PHPickerResultLoader.swift @@ -10,12 +10,13 @@ import Foundation import Combine import MobileCoreServices import PhotosUI +import MastodonSDK // load image with low memory usage // Refs: https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/ enum PHPickerResultLoader { - static func loadImageData(from result: PHPickerResult) -> Future { + static func loadImageData(from result: PHPickerResult) -> Future { Future { promise in result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in if let error = error { @@ -64,7 +65,37 @@ enum PHPickerResultLoader { let dataSize = ByteCountFormatter.string(fromByteCount: Int64(data.length), countStyle: .memory) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: load image %s", ((#file as NSString).lastPathComponent), #line, #function, dataSize) - promise(.success(data as Data)) + let file = Mastodon.Query.MediaAttachment.jpeg(data as Data) + promise(.success(file)) + } + } + } + + static func loadVideoData(from result: PHPickerResult) -> Future { + Future { promise in + result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in + if let error = error { + promise(.failure(error)) + return + } + + guard let url = url else { + promise(.success(nil)) + return + } + + let fileName = UUID().uuidString + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.copyItem(at: url, to: fileURL) + let file = Mastodon.Query.MediaAttachment.other(fileURL, fileExtension: fileURL.pathExtension, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") + promise(.success(file)) + } catch { + promise(.failure(error)) + } + } } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift index 5ae344b3d..0918cbd07 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift @@ -42,11 +42,17 @@ extension Mastodon.API.Media { authorization: authorization ) request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment + let serialStream = query.serialStream + request.httpBodyStream = serialStream.boundStreams.input return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } + .handleEvents(receiveCancel: { + // retain and handle cancel task + serialStream.boundStreams.output.close() + }) .eraseToAnyPublisher() } @@ -73,15 +79,30 @@ extension Mastodon.API.Media { } var body: Data? { - var data = Data() - - file.flatMap { data.append(Data.multipart(key: "file", value: $0)) } - thumbnail.flatMap { data.append(Data.multipart(key: "thumbnail", value: $0)) } - description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } - focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } + // using stream data + return nil + } + + var serialStream: SerialStream { + var streams: [InputStream] = [] - data.append(Data.multipartEnd()) - return data + file.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "file", value: value))) + value.multipartStreamValue.flatMap { streams.append($0) } + } + thumbnail.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "thumbnail", value: value))) + value.multipartStreamValue.flatMap { streams.append($0) } + } + description.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "description", value: value))) + } + focus.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "focus", value: value))) + } + streams.append(InputStream(data: Data.multipartEnd())) + + return SerialStream(streams: streams) } } @@ -129,8 +150,45 @@ extension Mastodon.API.Media { } .eraseToAnyPublisher() } - - public typealias UpdateMediaQuery = UploadMeidaQuery + + public struct UpdateMediaQuery: PutQuery { + + public let file: Mastodon.Query.MediaAttachment? + public let thumbnail: Mastodon.Query.MediaAttachment? + public let description: String? + public let focus: String? + + public init( + file: Mastodon.Query.MediaAttachment?, + thumbnail: Mastodon.Query.MediaAttachment?, + description: String?, + focus: String? + ) { + self.file = file + self.thumbnail = thumbnail + self.description = description + self.focus = focus + } + + var contentType: String? { + return Self.multipartContentType() + } + + var queryItems: [URLQueryItem]? { + return nil + } + + var body: Data? { + var data = Data() + + // not modify uploaded binary data + description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } + focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } + + data.append(Data.multipartEnd()) + return data + } + } } diff --git a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift index 48e442b9d..35c66176d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift +++ b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift @@ -26,7 +26,12 @@ extension Data { data.append("Content-Type: \(contentType)\r\n".data(using: .utf8)!) } data.append("\r\n".data(using: .utf8)!) - data.append(value.multipartValue) + if value.multipartStreamValue == nil { + data.append(value.multipartValue) + } else { + // needs append stream multipart value outside + // seealso: SerialStream + } return data } diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift index f3bd88832..ca9388cac 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift @@ -16,21 +16,22 @@ extension Mastodon.Query { /// PNG (Portable Network Graphics) image case png(Data?) /// Other media file - case other(Data?, fileExtension: String, mimeType: String) + /// e.g video + case other(URL?, fileExtension: String, mimeType: String) } } extension Mastodon.Query.MediaAttachment { - var data: Data? { + public var data: Data? { switch self { case .jpeg(let data): return data case .gif(let data): return data case .png(let data): return data - case .other(let data, _, _): return data + case .other: return nil } } - var fileName: String { + public var fileName: String { let name = UUID().uuidString switch self { case .jpeg: return "\(name).jpg" @@ -40,7 +41,7 @@ extension Mastodon.Query.MediaAttachment { } } - var mimeType: String { + public var mimeType: String { switch self { case .jpeg: return "image/jpg" case .gif: return "image/gif" @@ -56,6 +57,14 @@ extension Mastodon.Query.MediaAttachment { extension Mastodon.Query.MediaAttachment: MultipartFormValue { var multipartValue: Data { return data ?? Data() } + var multipartStreamValue: InputStream? { + switch self { + case .other(let url, _, _): + return url.flatMap { InputStream(url: $0) } + default: + return nil + } + } var multipartContentType: String? { return mimeType } var multipartFilename: String? { return fileName } } diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift index f86a71c8e..73a0e9ed9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift @@ -13,10 +13,15 @@ enum Multipart { protocol MultipartFormValue { var multipartValue: Data { get } + var multipartStreamValue: InputStream? { get } var multipartContentType: String? { get } var multipartFilename: String? { get } } +extension MultipartFormValue { + var multipartStreamValue: InputStream? { nil } +} + extension Bool: MultipartFormValue { var multipartValue: Data { switch self { diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift new file mode 100644 index 000000000..ad86033e7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift @@ -0,0 +1,147 @@ +// +// SerialStream.swift +// +// +// Created by MainasuK Cirno on 2021-5-28. +// + +import os.log +import Foundation +import Combine + +// ref: +// - https://developer.apple.com/documentation/foundation/url_loading_system/uploading_streams_of_data#3037342 +// - https://forums.swift.org/t/extension-write-to-outputstream/42817/4 +// - https://gist.github.com/khanlou/b5e07f963bedcb6e0fcc5387b46991c3 + +final class SerialStream: NSObject { + var writingTimerSubscriber: AnyCancellable? + + // serial stream source + private var streams: [InputStream] + private var currentStreamIndex = 0 + + private static let bufferSize = 5 * 1024 * 1024 // 5MiB + + private var buffer: UnsafeMutablePointer + private var canWrite = false + + private let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.SerialStream.\(UUID().uuidString)") + + // bound pair stream + private(set) lazy var boundStreams: Streams = { + var inputStream: InputStream? + var outputStream: OutputStream? + Stream.getBoundStreams(withBufferSize: SerialStream.bufferSize, inputStream: &inputStream, outputStream: &outputStream) + guard let input = inputStream, let output = outputStream else { + fatalError() + } + + output.delegate = self + output.schedule(in: .current, forMode: .default) + output.open() + + return Streams(input: input, output: output) + }() + + init(streams: [InputStream]) { + self.streams = streams + self.buffer = UnsafeMutablePointer.allocate(capacity: SerialStream.bufferSize) + self.buffer.initialize(repeating: 0, count: SerialStream.bufferSize) + super.init() + + // Stream worker + writingTimerSubscriber = Timer.publish(every: 0.5, on: .current, in: .default) + .autoconnect() + .receive(on: workingQueue) + .sink { [weak self] timer in + guard let self = self else { return } + guard self.canWrite else { return } + os_log(.debug, "%{public}s[%{public}ld], %{public}s: writing…", ((#file as NSString).lastPathComponent), #line, #function) + + guard self.currentStreamIndex < self.streams.count else { + self.boundStreams.output.close() + self.writingTimerSubscriber = nil // cancel timer after task completed + return + } + + var readBytesCount = 0 + defer { + var baseAddress = 0 + var remainsBytes = readBytesCount + while remainsBytes > 0 { + let result = self.boundStreams.output.write(&self.buffer[baseAddress], maxLength: remainsBytes) + baseAddress += result + remainsBytes -= result + os_log(.debug, "%{public}s[%{public}ld], %{public}s: write %ld/%ld bytes. write result: %ld", ((#file as NSString).lastPathComponent), #line, #function, baseAddress, readBytesCount, result) + } + } + + while readBytesCount < SerialStream.bufferSize { + // close when no more source streams + guard self.currentStreamIndex < self.streams.count else { + break + } + + let inputStream = self.streams[self.currentStreamIndex] + // open input if needs + if inputStream.streamStatus != .open { + inputStream.open() + } + // read next source stream when current drain + guard inputStream.hasBytesAvailable else { + self.currentStreamIndex += 1 + continue + } + + let reaminsCount = SerialStream.bufferSize - readBytesCount + let readCount = inputStream.read(&self.buffer[readBytesCount], maxLength: reaminsCount) + os_log(.debug, "%{public}s[%{public}ld], %{public}s: read source %ld bytes", ((#file as NSString).lastPathComponent), #line, #function, readCount) + + switch readCount { + case 0: + self.currentStreamIndex += 1 + continue + case -1: + self.boundStreams.output.close() + return + default: + self.canWrite = false + readBytesCount += readCount + } + } + } + } + + deinit { + os_log(.debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension SerialStream { + struct Streams { + let input: InputStream + let output: OutputStream + } +} + +// MARK: - StreamDelegate +extension SerialStream: StreamDelegate { + func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + os_log(.debug, "%{public}s[%{public}ld], %{public}s: eventCode %s", ((#file as NSString).lastPathComponent), #line, #function, String(eventCode.rawValue)) + + guard aStream == boundStreams.output else { + return + } + + if eventCode.contains(.hasSpaceAvailable) { + canWrite = true + } + + if eventCode.contains(.errorOccurred) { + // Close the streams and alert the user that the upload failed. + boundStreams.output.close() + } + } +} diff --git a/Podfile b/Podfile index ea7075ea8..d9f295c29 100644 --- a/Podfile +++ b/Podfile @@ -13,6 +13,9 @@ target 'Mastodon' do pod 'SwiftGen', '~> 6.4.0' pod 'DateToolsSwift', '~> 5.0.0' pod 'Kanna', '~> 5.2.2' + + # DEBUG + pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] target 'MastodonTests' do inherit! :search_paths @@ -23,14 +26,16 @@ target 'Mastodon' do # Pods for testing end - target 'NotificationService' do +end - end - - target 'AppShared' do - - end +target 'NotificationService' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! +end +target 'AppShared' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! end plugin 'cocoapods-keys', { diff --git a/Podfile.lock b/Podfile.lock index e341a2420..ea8ecc821 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,6 @@ PODS: - DateToolsSwift (5.0.0) + - FLEX (4.4.1) - Kanna (5.2.4) - Keys (1.0.1) - SwiftGen (6.4.0) @@ -7,6 +8,7 @@ PODS: DEPENDENCIES: - DateToolsSwift (~> 5.0.0) + - FLEX (~> 4.4.0) - Kanna (~> 5.2.2) - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) @@ -15,6 +17,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - DateToolsSwift + - FLEX - Kanna - SwiftGen - "UITextField+Shake" @@ -25,11 +28,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 + FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: a8dbae22e6e0bfb84f7db59aef1aa1716793d287 +PODFILE CHECKSUM: 0daad1778e56099e7a7e7ebe3d292d20051840fa COCOAPODS: 1.10.1