feat: add video attachment post supports

This commit is contained in:
CMK 2021-05-31 16:42:49 +08:00
parent 90d8de1625
commit a9744146ce
22 changed files with 606 additions and 179 deletions

View File

@ -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",

View File

@ -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 = "<group>"; };
2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = "<group>"; };
5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = "<group>"; };
@ -741,9 +741,11 @@
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -1036,6 +1038,8 @@
DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewHeaderFooterView.swift; sourceTree = "<group>"; };
DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = "<group>"; };
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>";
@ -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 = "<group>";
@ -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 */

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>14</integer>
<integer>15</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
@ -32,7 +32,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>15</integer>
<integer>16</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -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
}

View File

@ -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.

View File

@ -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";

View File

@ -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";

View File

@ -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

View File

@ -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?) {

View File

@ -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
}()

View File

@ -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()

View File

@ -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)
}

View File

@ -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

View File

@ -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<Mastodon.Query.MediaAttachment?, Never>(nil)
let description = CurrentValueSubject<String?, Never>(nil)
// output
// TODO: handle video/GIF/Audio data
let imageData = CurrentValueSubject<Data?, Never>(nil)
let thumbnailImage = CurrentValueSubject<UIImage?, Never>(nil)
let attachment = CurrentValueSubject<Mastodon.Entity.Attachment?, Never>(nil)
let description = CurrentValueSubject<String?, Never>(nil)
let error = CurrentValueSubject<Error?, Never>(nil)
private(set) lazy var uploadStateMachine: GKStateMachine = {
@ -58,7 +60,16 @@ final class MastodonAttachmentService {
setupServiceObserver()
PHPickerResultLoader.loadImageData(from: pickerResult)
Just(pickerResult)
.flatMap { result -> AnyPublisher<Mastodon.Query.MediaAttachment?, Error> 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 {

View File

@ -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<Data?, Error> {
static func loadImageData(from result: PHPickerResult) -> Future<Mastodon.Query.MediaAttachment?, Error> {
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<Mastodon.Query.MediaAttachment?, Error> {
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))
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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 }
}

View File

@ -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 {

View File

@ -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<UInt8>
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<UInt8>.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()
}
}
}

17
Podfile
View File

@ -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', {

View File

@ -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