Merge pull request #72 from tootsuite/feature/compose-attachment-image
Add compose attachment image status feature
This commit is contained in:
commit
93fe9ce30c
|
@ -194,8 +194,20 @@
|
||||||
"new_post": "New Post",
|
"new_post": "New Post",
|
||||||
"new_reply": "New Reply"
|
"new_reply": "New Reply"
|
||||||
},
|
},
|
||||||
|
"media_selection": {
|
||||||
|
"camera": "Take Photo",
|
||||||
|
"photo_library": "Photo Library",
|
||||||
|
"browse": "Browse"
|
||||||
|
},
|
||||||
"content_input_placeholder": "Type or paste what's on your mind",
|
"content_input_placeholder": "Type or paste what's on your mind",
|
||||||
"compose_action": "Publish"
|
"compose_action": "Publish",
|
||||||
|
"attachment": {
|
||||||
|
"photo": "photo",
|
||||||
|
"video": "video",
|
||||||
|
"attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.",
|
||||||
|
"description_photo": "Describe photo for low vision people...",
|
||||||
|
"description_video": "Describe what’s happening for low vision people..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -171,6 +171,8 @@
|
||||||
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
||||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
||||||
|
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
||||||
|
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */; };
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; };
|
||||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
||||||
|
@ -182,8 +184,9 @@
|
||||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
||||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
|
||||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */; };
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */; };
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
|
||||||
|
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
||||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||||
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
||||||
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
@ -214,6 +217,13 @@
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
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 */; };
|
||||||
|
DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488326034BD7008B817C /* APIService+Status.swift */; };
|
||||||
|
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 */; };
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
||||||
|
@ -455,6 +465,8 @@
|
||||||
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
||||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||||
|
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
||||||
|
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||||
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = "<group>"; };
|
||||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -466,8 +478,9 @@
|
||||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
|
||||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTootContentTableViewCell.swift; sourceTree = "<group>"; };
|
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentTableViewCell.swift; sourceTree = "<group>"; };
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
||||||
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -500,6 +513,12 @@
|
||||||
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||||
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
||||||
|
DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHPickerResultLoader.swift; sourceTree = "<group>"; };
|
||||||
|
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
|
||||||
|
DB9A488326034BD7008B817C /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = "<group>"; };
|
||||||
|
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
|
||||||
|
DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = "<group>"; };
|
||||||
|
DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAttachmentService+UploadState.swift"; sourceTree = "<group>"; };
|
||||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -530,6 +549,7 @@
|
||||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||||
|
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
||||||
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
|
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
|
||||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||||
|
@ -723,6 +743,7 @@
|
||||||
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
||||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
|
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
|
||||||
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */,
|
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */,
|
||||||
|
DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */,
|
||||||
);
|
);
|
||||||
path = Vender;
|
path = Vender;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -731,6 +752,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||||
|
DB49A61925FF327D00B98345 /* EmojiService */,
|
||||||
|
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
||||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||||
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
||||||
|
@ -1032,6 +1055,8 @@
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
||||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
|
||||||
|
DB9A488326034BD7008B817C /* APIService+Status.swift */,
|
||||||
|
DB9A488F26035963008B817C /* APIService+Media.swift */,
|
||||||
);
|
);
|
||||||
path = APIService;
|
path = APIService;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1068,6 +1093,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
|
||||||
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
|
||||||
|
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1111,21 +1138,23 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB55D32225FB4D320002F825 /* View */,
|
DB55D32225FB4D320002F825 /* View */,
|
||||||
DB789A2125F9F76D0071ACA0 /* TableViewCell */,
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
||||||
|
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
|
||||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
||||||
);
|
);
|
||||||
path = Compose;
|
path = Compose;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB789A2125F9F76D0071ACA0 /* TableViewCell */ = {
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */,
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */,
|
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
||||||
|
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = TableViewCell;
|
path = CollectionViewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
||||||
|
@ -1284,6 +1313,15 @@
|
||||||
path = Generated;
|
path = Generated;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB9A489B26036E19008B817C /* MastodonAttachmentService */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */,
|
||||||
|
DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */,
|
||||||
|
);
|
||||||
|
path = MastodonAttachmentService;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB9D6BEE25E4F5370051B173 /* Search */ = {
|
DB9D6BEE25E4F5370051B173 /* Search */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1407,6 +1445,7 @@
|
||||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
||||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||||
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
|
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
|
||||||
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -1537,6 +1576,7 @@
|
||||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
||||||
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
||||||
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||||
|
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -1730,6 +1770,7 @@
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
||||||
|
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
|
||||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||||
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
|
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
|
||||||
|
@ -1748,6 +1789,7 @@
|
||||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
|
||||||
|
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
||||||
|
@ -1774,6 +1816,7 @@
|
||||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
|
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
||||||
|
@ -1787,6 +1830,7 @@
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||||
|
DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */,
|
||||||
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
|
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||||
|
@ -1807,6 +1851,7 @@
|
||||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||||
|
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
||||||
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
||||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||||
|
@ -1817,8 +1862,9 @@
|
||||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */,
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
|
||||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||||
|
DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */,
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
|
@ -1842,6 +1888,7 @@
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||||
|
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
|
||||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
||||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||||
|
@ -1879,10 +1926,12 @@
|
||||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
|
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||||
|
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
|
@ -1894,7 +1943,7 @@
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */,
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -2489,6 +2538,14 @@
|
||||||
minimumVersion = 1.0.0;
|
minimumVersion = 1.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.4.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -2536,6 +2593,11 @@
|
||||||
package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
|
||||||
productName = TwitterTextEditor;
|
productName = TwitterTextEditor;
|
||||||
};
|
};
|
||||||
|
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||||
|
productName = "UITextView+Placeholder";
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|
|
@ -108,6 +108,15 @@
|
||||||
"revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91",
|
"revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "UITextView+Placeholder",
|
||||||
|
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
|
||||||
|
"version": "1.4.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,7 @@ import CoreData
|
||||||
enum ComposeStatusItem {
|
enum ComposeStatusItem {
|
||||||
case replyTo(statusObjectID: NSManagedObjectID)
|
case replyTo(statusObjectID: NSManagedObjectID)
|
||||||
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
||||||
|
case attachment(attachmentService: MastodonAttachmentService)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusItem: Hashable { }
|
extension ComposeStatusItem: Hashable { }
|
||||||
|
|
|
@ -10,10 +10,12 @@ import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
enum ComposeStatusSection: Equatable, Hashable {
|
enum ComposeStatusSection: Equatable, Hashable {
|
||||||
case repliedTo
|
case repliedTo
|
||||||
case status
|
case status
|
||||||
|
case attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
|
@ -24,21 +26,23 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
static func tableViewDiffableDataSource(
|
|
||||||
for tableView: UITableView,
|
static func collectionViewDiffableDataSource(
|
||||||
|
for collectionView: UICollectionView,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
managedObjectContext: NSManagedObjectContext,
|
managedObjectContext: NSManagedObjectContext,
|
||||||
composeKind: ComposeKind,
|
composeKind: ComposeKind,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
) -> UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
|
||||||
UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>(tableView: tableView) { [weak textEditorViewTextAttributesDelegate] tableView, indexPath, item -> UITableViewCell? in
|
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
||||||
|
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
case .replyTo(let repliedToStatusObjectID):
|
case .replyTo(let repliedToStatusObjectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToTootContentCollectionViewCell
|
||||||
// TODO:
|
|
||||||
return cell
|
return cell
|
||||||
case .input(let replyToTootObjectID, let attribute):
|
case .input(let replyToTootObjectID, let attribute):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeTootContentTableViewCell.self), for: indexPath) as! ComposeTootContentTableViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
||||||
|
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
||||||
managedObjectContext.perform {
|
managedObjectContext.perform {
|
||||||
guard let replyToTootObjectID = replyToTootObjectID,
|
guard let replyToTootObjectID = replyToTootObjectID,
|
||||||
let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
|
let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
|
||||||
|
@ -52,13 +56,74 @@ extension ComposeStatusSection {
|
||||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||||
// self size input cell
|
// self size input cell
|
||||||
cell.composeContent
|
cell.composeContent
|
||||||
|
.removeDuplicates()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { text in
|
.sink { text in
|
||||||
tableView.beginUpdates()
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
tableView.endUpdates()
|
// bind input data
|
||||||
|
attribute.composeContent.value = text
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
return cell
|
return cell
|
||||||
|
case .attachment(let attachmentService):
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
|
||||||
|
cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
|
||||||
|
cell.delegate = composeStatusAttachmentTableViewCellDelegate
|
||||||
|
attachmentService.imageData
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { imageData in
|
||||||
|
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 {
|
||||||
|
let placeholder = UIImage.placeholder(
|
||||||
|
size: size,
|
||||||
|
color: Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
)
|
||||||
|
.af.imageRounded(
|
||||||
|
withCornerRadius: AttachmentContainerView.containerViewCornerRadius
|
||||||
|
)
|
||||||
|
cell.attachmentContainerView.previewImageView.image = placeholder
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cell.attachmentContainerView.previewImageView.image = image
|
||||||
|
.af.imageAspectScaled(toFill: size)
|
||||||
|
.af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
|
||||||
|
attachmentService.error.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { uploadState, error in
|
||||||
|
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
|
||||||
|
cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
|
||||||
|
if let _ = error {
|
||||||
|
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||||
|
} else {
|
||||||
|
guard let uploadState = uploadState else { return }
|
||||||
|
switch uploadState {
|
||||||
|
case is MastodonAttachmentService.UploadState.Finish,
|
||||||
|
is MastodonAttachmentService.UploadState.Fail:
|
||||||
|
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
NotificationCenter.default.publisher(
|
||||||
|
for: UITextView.textDidChangeNotification,
|
||||||
|
object: cell.attachmentContainerView.descriptionTextView
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { notification in
|
||||||
|
guard let textField = notification.object as? UITextView else { return }
|
||||||
|
let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
attachmentService.description.value = text
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +131,7 @@ extension ComposeStatusSection {
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: ComposeTootContentTableViewCell,
|
cell: ComposeStatusContentCollectionViewCell,
|
||||||
attribute: ComposeStatusItem.ComposeStatusAttribute
|
attribute: ComposeStatusItem.ComposeStatusAttribute
|
||||||
) {
|
) {
|
||||||
// set avatar
|
// set avatar
|
||||||
|
|
|
@ -30,11 +30,16 @@ internal enum Asset {
|
||||||
}
|
}
|
||||||
internal enum Colors {
|
internal enum Colors {
|
||||||
internal enum Background {
|
internal enum Background {
|
||||||
|
internal enum AudioPlayer {
|
||||||
|
internal static let highlight = ColorAsset(name: "Colors/Background/AudioPlayer/highlight")
|
||||||
|
}
|
||||||
internal enum Poll {
|
internal enum Poll {
|
||||||
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
||||||
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
||||||
}
|
}
|
||||||
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/mediaTypeIndicotor")
|
internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border")
|
||||||
|
internal static let danger = ColorAsset(name: "Colors/Background/danger")
|
||||||
|
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
|
||||||
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
||||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||||
|
@ -45,7 +50,6 @@ internal enum Asset {
|
||||||
internal enum Button {
|
internal enum Button {
|
||||||
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
||||||
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
|
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
|
||||||
internal static let highlight = ColorAsset(name: "Colors/Button/highlight")
|
|
||||||
internal static let normal = ColorAsset(name: "Colors/Button/normal")
|
internal static let normal = ColorAsset(name: "Colors/Button/normal")
|
||||||
}
|
}
|
||||||
internal enum Icon {
|
internal enum Icon {
|
||||||
|
@ -79,10 +83,12 @@ internal enum Asset {
|
||||||
internal static let lightSecondaryText = ColorAsset(name: "Colors/lightSecondaryText")
|
internal static let lightSecondaryText = ColorAsset(name: "Colors/lightSecondaryText")
|
||||||
internal static let lightSuccessGreen = ColorAsset(name: "Colors/lightSuccessGreen")
|
internal static let lightSuccessGreen = ColorAsset(name: "Colors/lightSuccessGreen")
|
||||||
internal static let lightWhite = ColorAsset(name: "Colors/lightWhite")
|
internal static let lightWhite = ColorAsset(name: "Colors/lightWhite")
|
||||||
internal static let plusCircleFill = ImageAsset(name: "Colors/plus.circle.fill")
|
|
||||||
internal static let systemGreen = ColorAsset(name: "Colors/system.green")
|
internal static let systemGreen = ColorAsset(name: "Colors/system.green")
|
||||||
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
|
||||||
}
|
}
|
||||||
|
internal enum Connectivity {
|
||||||
|
internal static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split")
|
||||||
|
}
|
||||||
internal enum Welcome {
|
internal enum Welcome {
|
||||||
internal enum Illustration {
|
internal enum Illustration {
|
||||||
internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
|
internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
|
||||||
|
|
|
@ -148,6 +148,28 @@ internal enum L10n {
|
||||||
internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
|
internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
|
||||||
/// Type or paste what's on your mind
|
/// Type or paste what's on your mind
|
||||||
internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
|
internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
|
||||||
|
internal enum Attachment {
|
||||||
|
/// This %@ is broken and can't be\nuploaded to Mastodon.
|
||||||
|
internal static func attachmentBroken(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1))
|
||||||
|
}
|
||||||
|
/// Describe photo for low vision people...
|
||||||
|
internal static let descriptionPhoto = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionPhoto")
|
||||||
|
/// Describe what’s happening for low vision people...
|
||||||
|
internal static let descriptionVideo = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionVideo")
|
||||||
|
/// photo
|
||||||
|
internal static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo")
|
||||||
|
/// video
|
||||||
|
internal static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video")
|
||||||
|
}
|
||||||
|
internal enum MediaSelection {
|
||||||
|
/// Browse
|
||||||
|
internal static let browse = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Browse")
|
||||||
|
/// Take Photo
|
||||||
|
internal static let camera = L10n.tr("Localizable", "Scene.Compose.MediaSelection.Camera")
|
||||||
|
/// Photo Library
|
||||||
|
internal static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary")
|
||||||
|
}
|
||||||
internal enum Title {
|
internal enum Title {
|
||||||
/// New Post
|
/// New Post
|
||||||
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")
|
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")
|
||||||
|
|
|
@ -2,20 +2,14 @@
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "plus.circle.fill.pdf",
|
"filename" : "plus.circle.fill.pdf",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal"
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
"version" : 1
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.839",
|
"blue" : "0.851",
|
||||||
"green" : "0.573",
|
"green" : "0.565",
|
||||||
"red" : "0.204"
|
"red" : "0.169"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "66",
|
||||||
|
"green" : "46",
|
||||||
|
"red" : "163"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "90",
|
||||||
|
"green" : "64",
|
||||||
|
"red" : "223"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0xE8",
|
"blue" : "232",
|
||||||
"green" : "0xE1",
|
"green" : "225",
|
||||||
"red" : "0xD9"
|
"red" : "217"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -11,6 +11,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xFF",
|
||||||
|
"green" : "0x84",
|
||||||
|
"red" : "0x0A"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "plus.circle.fill.pdf",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
%PDF-1.7
|
|
||||||
|
|
||||||
1 0 obj
|
|
||||||
<< >>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
2 0 obj
|
|
||||||
<< /Length 3 0 R >>
|
|
||||||
stream
|
|
||||||
/DeviceRGB CS
|
|
||||||
/DeviceRGB cs
|
|
||||||
q
|
|
||||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
|
||||||
1.000000 1.000000 1.000000 scn
|
|
||||||
30.000000 15.000000 m
|
|
||||||
30.000000 6.715729 23.284271 0.000000 15.000000 0.000000 c
|
|
||||||
6.715729 0.000000 0.000000 6.715729 0.000000 15.000000 c
|
|
||||||
0.000000 23.284271 6.715729 30.000000 15.000000 30.000000 c
|
|
||||||
23.284271 30.000000 30.000000 23.284271 30.000000 15.000000 c
|
|
||||||
h
|
|
||||||
f
|
|
||||||
n
|
|
||||||
Q
|
|
||||||
q
|
|
||||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
|
||||||
0.000000 0.000000 0.000000 scn
|
|
||||||
15.009004 0.000000 m
|
|
||||||
23.233341 0.000000 30.000000 6.766640 30.000000 15.009003 c
|
|
||||||
30.000000 23.233379 23.233341 30.000000 14.991017 30.000000 c
|
|
||||||
6.766642 30.000000 0.000000 23.233379 0.000000 15.009003 c
|
|
||||||
0.000000 6.766640 6.766643 0.000000 15.009004 0.000000 c
|
|
||||||
h
|
|
||||||
8.098384 15.009003 m
|
|
||||||
8.098384 16.034798 8.836217 16.790653 9.844025 16.790653 c
|
|
||||||
13.209368 16.790653 l
|
|
||||||
13.209368 20.155996 l
|
|
||||||
13.209368 21.163769 13.965223 21.919624 14.991017 21.919624 c
|
|
||||||
16.016811 21.919624 16.772667 21.163769 16.772667 20.155996 c
|
|
||||||
16.772667 16.790653 l
|
|
||||||
20.137974 16.790653 l
|
|
||||||
21.163769 16.790653 21.901638 16.034798 21.901638 15.009003 c
|
|
||||||
21.901638 13.983208 21.163769 13.227352 20.137974 13.227352 c
|
|
||||||
16.772667 13.227352 l
|
|
||||||
16.772667 9.862047 l
|
|
||||||
16.772667 8.854239 16.016811 8.098381 14.991017 8.098381 c
|
|
||||||
13.965223 8.098381 13.209368 8.854239 13.209368 9.862047 c
|
|
||||||
13.209368 13.227352 l
|
|
||||||
9.844025 13.227352 l
|
|
||||||
8.836217 13.227352 8.098384 13.983208 8.098384 15.009003 c
|
|
||||||
h
|
|
||||||
f
|
|
||||||
n
|
|
||||||
Q
|
|
||||||
|
|
||||||
endstream
|
|
||||||
endobj
|
|
||||||
|
|
||||||
3 0 obj
|
|
||||||
1426
|
|
||||||
endobj
|
|
||||||
|
|
||||||
4 0 obj
|
|
||||||
<< /Annots []
|
|
||||||
/Type /Page
|
|
||||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
|
||||||
/Resources 1 0 R
|
|
||||||
/Contents 2 0 R
|
|
||||||
/Parent 5 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
5 0 obj
|
|
||||||
<< /Kids [ 4 0 R ]
|
|
||||||
/Count 1
|
|
||||||
/Type /Pages
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
6 0 obj
|
|
||||||
<< /Type /Catalog
|
|
||||||
/Pages 5 0 R
|
|
||||||
>>
|
|
||||||
endobj
|
|
||||||
|
|
||||||
xref
|
|
||||||
0 7
|
|
||||||
0000000000 65535 f
|
|
||||||
0000000010 00000 n
|
|
||||||
0000000034 00000 n
|
|
||||||
0000001516 00000 n
|
|
||||||
0000001539 00000 n
|
|
||||||
0000001712 00000 n
|
|
||||||
0000001786 00000 n
|
|
||||||
trailer
|
|
||||||
<< /ID [ (some) (id) ]
|
|
||||||
/Root 6 0 R
|
|
||||||
/Size 7
|
|
||||||
>>
|
|
||||||
startxref
|
|
||||||
1845
|
|
||||||
%%EOF
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
15
Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Contents.json
vendored
Normal file
15
Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Frame 2.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
114
Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Frame 2.pdf
vendored
Normal file
114
Mastodon/Resources/Assets.xcassets/Connectivity/photo.fill.split.imageset/Frame 2.pdf
vendored
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
0.992546 -0.121869 0.121869 0.992546 42.624641 7.462139 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
29.841717 4.404652 m
|
||||||
|
10.813622 4.404652 l
|
||||||
|
5.729721 9.441498 l
|
||||||
|
29.810324 9.441498 l
|
||||||
|
32.782593 9.441498 34.628483 11.256016 34.628483 14.259354 c
|
||||||
|
34.628483 19.077240 l
|
||||||
|
20.237179 32.404518 l
|
||||||
|
18.766808 33.781090 16.983574 34.438072 15.262939 34.438072 c
|
||||||
|
13.481962 34.438072 11.763362 33.813934 10.231857 32.441067 c
|
||||||
|
0.000000 39.493633 l
|
||||||
|
11.853184 50.706104 l
|
||||||
|
1.586006 62.000000 l
|
||||||
|
29.841717 62.000000 l
|
||||||
|
36.411587 62.000000 39.665127 58.746330 39.665127 52.301659 c
|
||||||
|
39.665127 14.102936 l
|
||||||
|
39.665127 7.658268 36.411587 4.404652 29.841717 4.404652 c
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 6.000000 11.404663 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
35.690556 57.595337 m
|
||||||
|
9.823408 57.595337 l
|
||||||
|
3.284870 57.595337 0.000000 54.372997 0.000000 47.896996 c
|
||||||
|
0.000000 9.698273 l
|
||||||
|
0.000000 3.222317 3.284870 -0.000011 9.823408 -0.000011 c
|
||||||
|
44.918179 -0.000011 l
|
||||||
|
39.834278 5.036835 l
|
||||||
|
9.886006 5.036835 l
|
||||||
|
6.851334 5.036835 5.036836 6.851357 5.036836 9.917267 c
|
||||||
|
5.036836 11.825638 l
|
||||||
|
14.641250 20.209938 l
|
||||||
|
16.017820 21.430046 17.519461 22.055767 18.896032 22.055767 c
|
||||||
|
20.428938 22.055767 22.024504 21.430050 23.401012 20.147408 c
|
||||||
|
29.376427 14.766380 l
|
||||||
|
44.330532 28.031185 l
|
||||||
|
44.332489 28.032942 44.334446 28.034697 44.336403 28.036451 c
|
||||||
|
34.104435 35.089096 l
|
||||||
|
45.957619 46.301567 l
|
||||||
|
35.690556 57.595337 l
|
||||||
|
h
|
||||||
|
15.736227 35.758499 m
|
||||||
|
15.736227 31.503782 19.208826 28.031185 23.463608 28.031185 c
|
||||||
|
27.687059 28.031185 31.159658 31.503782 31.159658 35.758499 c
|
||||||
|
31.159658 39.981949 27.687059 43.485878 23.463608 43.485878 c
|
||||||
|
19.208826 43.485878 15.736227 39.981949 15.736227 35.758499 c
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
1681
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 92.000000 76.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Type /Catalog
|
||||||
|
/Pages 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000001771 00000 n
|
||||||
|
0000001794 00000 n
|
||||||
|
0000001967 00000 n
|
||||||
|
0000002041 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2100
|
||||||
|
%%EOF
|
|
@ -39,8 +39,17 @@
|
||||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||||
"Common.Countable.Photo.Multiple" = "photos";
|
"Common.Countable.Photo.Multiple" = "photos";
|
||||||
"Common.Countable.Photo.Single" = "photo";
|
"Common.Countable.Photo.Single" = "photo";
|
||||||
|
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||||
|
uploaded to Mastodon.";
|
||||||
|
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
|
||||||
|
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||||
|
"Scene.Compose.Attachment.Photo" = "photo";
|
||||||
|
"Scene.Compose.Attachment.Video" = "video";
|
||||||
"Scene.Compose.ComposeAction" = "Publish";
|
"Scene.Compose.ComposeAction" = "Publish";
|
||||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||||
|
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
||||||
|
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||||
|
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||||
"Scene.Compose.Title.NewPost" = "New Post";
|
"Scene.Compose.Title.NewPost" = "New Post";
|
||||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||||
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// ComposeRepliedToTootContentCollectionViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ComposeRepliedToTootContentCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeRepliedToTootContentCollectionViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusAttachmentTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol ComposeStatusAttachmentCollectionViewCellDelegate: class {
|
||||||
|
func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
static let verticalMarginHeight: CGFloat = ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height * 0.5
|
||||||
|
static let removeButtonSize = CGSize(width: 22, height: 22)
|
||||||
|
|
||||||
|
weak var delegate: ComposeStatusAttachmentCollectionViewCellDelegate?
|
||||||
|
|
||||||
|
let attachmentContainerView = AttachmentContainerView()
|
||||||
|
let removeButton: UIButton = {
|
||||||
|
let button = HighlightDimmableButton()
|
||||||
|
button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||||
|
let image = UIImage(systemName: "minus")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))
|
||||||
|
button.tintColor = .white
|
||||||
|
button.setImage(image, for: .normal)
|
||||||
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Background.danger.color), for: .normal)
|
||||||
|
button.layer.masksToBounds = true
|
||||||
|
button.layer.cornerRadius = ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width * 0.5
|
||||||
|
button.layer.borderColor = Asset.Colors.Background.dangerBorder.color.cgColor
|
||||||
|
button.layer.borderWidth = 1
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
attachmentContainerView.activityIndicatorView.startAnimating()
|
||||||
|
attachmentContainerView.previewImageView.af.cancelImageRequest()
|
||||||
|
attachmentContainerView.previewImageView.image = .placeholder(color: .systemFill)
|
||||||
|
delegate = nil
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusAttachmentCollectionViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
// selectionStyle = .none
|
||||||
|
|
||||||
|
attachmentContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(attachmentContainerView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
attachmentContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
|
||||||
|
attachmentContainerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
attachmentContainerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: attachmentContainerView.bottomAnchor, constant: ComposeStatusAttachmentCollectionViewCell.verticalMarginHeight),
|
||||||
|
attachmentContainerView.heightAnchor.constraint(equalToConstant: 205).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
removeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(removeButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
removeButton.centerXAnchor.constraint(equalTo: attachmentContainerView.trailingAnchor),
|
||||||
|
removeButton.centerYAnchor.constraint(equalTo: attachmentContainerView.topAnchor),
|
||||||
|
removeButton.widthAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.width).priority(.defaultHigh),
|
||||||
|
removeButton.heightAnchor.constraint(equalToConstant: ComposeStatusAttachmentCollectionViewCell.removeButtonSize.height).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
removeButton.addTarget(self, action: #selector(ComposeStatusAttachmentCollectionViewCell.removeButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension ComposeStatusAttachmentCollectionViewCell {
|
||||||
|
|
||||||
|
@objc private func removeButtonDidPressed(_ sender: UIButton) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeStatusAttachmentCollectionViewCell(self, removeButtonDidPressed: sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// ComposeTootContentTableViewCell.swift
|
// ComposeStatusContentCollectionViewCell.swift
|
||||||
// Mastodon
|
// Mastodon
|
||||||
//
|
//
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
|
@ -9,7 +9,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
|
|
||||||
final class ComposeTootContentTableViewCell: UITableViewCell {
|
final class ComposeStatusContentCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ final class ComposeTootContentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
let composeContent = PassthroughSubject<String, Never>()
|
let composeContent = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(frame: CGRect) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +39,11 @@ final class ComposeTootContentTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeTootContentTableViewCell {
|
extension ComposeStatusContentCollectionViewCell {
|
||||||
|
|
||||||
private func _init() {
|
private func _init() {
|
||||||
selectionStyle = .none
|
// selectionStyle = .none
|
||||||
|
preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(statusView)
|
contentView.addSubview(statusView)
|
||||||
|
@ -56,6 +57,9 @@ extension ComposeTootContentTableViewCell {
|
||||||
statusView.nameTrialingDotLabel.isHidden = true
|
statusView.nameTrialingDotLabel.isHidden = true
|
||||||
statusView.dateLabel.isHidden = true
|
statusView.dateLabel.isHidden = true
|
||||||
|
|
||||||
|
statusView.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
|
statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
textEditorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(textEditorView)
|
contentView.addSubview(textEditorView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -65,6 +69,7 @@ extension ComposeTootContentTableViewCell {
|
||||||
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
|
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
|
||||||
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
|
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
|
|
||||||
|
@ -78,12 +83,8 @@ extension ComposeTootContentTableViewCell {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeTootContentTableViewCell {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITextViewDelegate
|
// MARK: - UITextViewDelegate
|
||||||
extension ComposeTootContentTableViewCell: TextEditorViewChangeObserver {
|
extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver {
|
||||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||||
guard changeResult.isTextChanged else { return }
|
guard changeResult.isTextChanged else { return }
|
||||||
composeContent.send(textEditorView.text)
|
composeContent.send(textEditorView.text)
|
|
@ -8,8 +8,9 @@
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import TwitterTextEditor
|
import PhotosUI
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import TwitterTextEditor
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
private var suffixedAttachmentViews: [UIView] = []
|
private var suffixedAttachmentViews: [UIView] = []
|
||||||
|
|
||||||
let composeTootBarButtonItem: UIBarButtonItem = {
|
let publishButton: UIButton = {
|
||||||
let button = RoundedEdgesButton(type: .custom)
|
let button = RoundedEdgesButton(type: .custom)
|
||||||
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
|
||||||
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
|
||||||
|
@ -31,17 +32,21 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
button.setTitleColor(.white, for: .normal)
|
button.setTitleColor(.white, for: .normal)
|
||||||
button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
|
button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
|
||||||
button.adjustsImageWhenHighlighted = false
|
button.adjustsImageWhenHighlighted = false
|
||||||
let barButtonItem = UIBarButtonItem(customView: button)
|
return button
|
||||||
|
}()
|
||||||
|
private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
|
||||||
|
let barButtonItem = UIBarButtonItem(customView: publishButton)
|
||||||
return barButtonItem
|
return barButtonItem
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let tableView: UITableView = {
|
let collectionView: UICollectionView = {
|
||||||
let tableView = ControlContainableTableView()
|
let collectionViewLayout = ComposeViewController.createLayout()
|
||||||
tableView.register(ComposeRepliedToTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self))
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
tableView.register(ComposeTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeTootContentTableViewCell.self))
|
collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
||||||
tableView.separatorStyle = .none
|
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
||||||
return tableView
|
collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
return collectionView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let composeToolbarView: ComposeToolbarView = {
|
let composeToolbarView: ComposeToolbarView = {
|
||||||
|
@ -56,6 +61,42 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
return backgroundView
|
return backgroundView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var imagePicker: PHPickerViewController = {
|
||||||
|
var configuration = PHPickerConfiguration()
|
||||||
|
configuration.filter = .images
|
||||||
|
configuration.selectionLimit = 4
|
||||||
|
|
||||||
|
let imagePicker = PHPickerViewController(configuration: configuration)
|
||||||
|
imagePicker.delegate = self
|
||||||
|
return imagePicker
|
||||||
|
}()
|
||||||
|
private(set) lazy var imagePickerController: UIImagePickerController = {
|
||||||
|
let imagePickerController = UIImagePickerController()
|
||||||
|
imagePickerController.sourceType = .camera
|
||||||
|
imagePickerController.delegate = self
|
||||||
|
return imagePickerController
|
||||||
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
|
||||||
|
let documentPickerController = UIDocumentPickerViewController(documentTypes: ["public.image"], in: .open)
|
||||||
|
documentPickerController.delegate = self
|
||||||
|
return documentPickerController
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeViewController {
|
||||||
|
private static func createLayout() -> UICollectionViewLayout {
|
||||||
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100))
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
// section.interGroupSpacing = 10
|
||||||
|
// section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
|
||||||
|
return UICollectionViewCompositionalLayout(section: section)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
@ -72,15 +113,16 @@ extension ComposeViewController {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
view.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
|
||||||
navigationItem.rightBarButtonItem = composeTootBarButtonItem
|
navigationItem.rightBarButtonItem = publishBarButtonItem
|
||||||
|
publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
|
||||||
|
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(tableView)
|
view.addSubview(collectionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -104,11 +146,15 @@ extension ComposeViewController {
|
||||||
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
|
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
tableView.delegate = self
|
collectionView.delegate = self
|
||||||
|
// Note: do not allow reorder due to the images display order following the upload time
|
||||||
|
// let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
||||||
|
// collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
for: tableView,
|
for: collectionView,
|
||||||
dependency: self,
|
dependency: self,
|
||||||
textEditorViewTextAttributesDelegate: self
|
textEditorViewTextAttributesDelegate: self,
|
||||||
|
composeStatusAttachmentTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
|
|
||||||
// respond scrollView overlap change
|
// respond scrollView overlap change
|
||||||
|
@ -123,31 +169,31 @@ extension ComposeViewController {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
guard isShow, state == .dock else {
|
guard isShow, state == .dock else {
|
||||||
self.tableView.contentInset.bottom = 0.0
|
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom
|
||||||
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
|
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
|
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// isShow AND dock state
|
// isShow AND dock state
|
||||||
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
|
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
|
||||||
let padding = contentFrame.maxY - endFrame.minY
|
let padding = contentFrame.maxY - endFrame.minY
|
||||||
guard padding > 0 else {
|
guard padding > 0 else {
|
||||||
self.tableView.contentInset.bottom = 0.0
|
self.collectionView.contentInset.bottom = self.view.safeAreaInsets.bottom
|
||||||
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
|
self.collectionView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
|
self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// add 16pt margin
|
// add 16pt margin
|
||||||
self.tableView.contentInset.bottom = padding + 16
|
self.collectionView.contentInset.bottom = padding + 16
|
||||||
self.tableView.verticalScrollIndicatorInsets.bottom = padding + 16
|
self.collectionView.verticalScrollIndicatorInsets.bottom = padding + 16
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
self.composeToolbarViewBottomLayoutConstraint.constant = padding
|
self.composeToolbarViewBottomLayoutConstraint.constant = padding
|
||||||
self.view.layoutIfNeeded()
|
self.view.layoutIfNeeded()
|
||||||
|
@ -155,9 +201,9 @@ extension ComposeViewController {
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
viewModel.isComposeTootBarButtonItemEnabled
|
viewModel.isPublishBarButtonItemEnabled
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.isEnabled, on: composeTootBarButtonItem)
|
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind custom emojis
|
// bind custom emojis
|
||||||
|
@ -173,6 +219,16 @@ extension ComposeViewController {
|
||||||
self.textEditorView()?.setNeedsUpdateTextAttributes()
|
self.textEditorView()?.setNeedsUpdateTextAttributes()
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind image picker toolbar state
|
||||||
|
viewModel.attachmentServices
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] attachmentServices in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.composeToolbarView.mediaButton.isEnabled = attachmentServices.count < 4
|
||||||
|
self.resetImagePicker()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -196,7 +252,7 @@ extension ComposeViewController {
|
||||||
switch item {
|
switch item {
|
||||||
case .input:
|
case .input:
|
||||||
guard let indexPath = diffableDataSource.indexPath(for: item),
|
guard let indexPath = diffableDataSource.indexPath(for: item),
|
||||||
let cell = tableView.cellForRow(at: indexPath) as? ComposeTootContentTableViewCell else {
|
let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusContentCollectionViewCell else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return cell.textEditorView
|
return cell.textEditorView
|
||||||
|
@ -227,6 +283,22 @@ extension ComposeViewController {
|
||||||
alertController.addAction(cancelAction)
|
alertController.addAction(cancelAction)
|
||||||
present(alertController, animated: true, completion: nil)
|
present(alertController, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
|
||||||
|
let imagePicker = PHPickerViewController(configuration: configuration)
|
||||||
|
imagePicker.delegate = self
|
||||||
|
return imagePicker
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
|
@ -240,6 +312,44 @@ extension ComposeViewController {
|
||||||
dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else {
|
||||||
|
// TODO: handle error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Do not allow reorder image due to image display order following the update time
|
||||||
|
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
||||||
|
switch(sender.state) {
|
||||||
|
case .began:
|
||||||
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
||||||
|
case .changed:
|
||||||
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||||
|
let diffableDataSource = viewModel.diffableDataSource else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
||||||
|
case .attachment = item else {
|
||||||
|
collectionView.cancelInteractiveMovement()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionView.updateInteractiveMovementTargetPosition(sender.location(in: collectionView))
|
||||||
|
case .ended:
|
||||||
|
collectionView.endInteractiveMovement()
|
||||||
|
default:
|
||||||
|
collectionView.cancelInteractiveMovement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - TextEditorViewTextAttributesDelegate
|
// MARK: - TextEditorViewTextAttributesDelegate
|
||||||
|
@ -280,32 +390,12 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
|
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
|
||||||
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
|
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
|
||||||
|
|
||||||
|
// hashtag
|
||||||
for match in highlightMatches {
|
for match in highlightMatches {
|
||||||
// hashtag
|
|
||||||
if let name = string.substring(with: match, at: 2) {
|
|
||||||
let attachment: TextAttributes.SuffixedAttachment?
|
|
||||||
switch name {
|
|
||||||
// FIXME:
|
|
||||||
case "person":
|
|
||||||
attachment = .init(size: CGSize(width: 20.0, height: 20.0),
|
|
||||||
attachment: .image(UIImage(systemName: "person")!))
|
|
||||||
default:
|
|
||||||
attachment = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let attachment = attachment {
|
|
||||||
let index = match.range.upperBound - 1
|
|
||||||
attributedString.addAttribute(
|
|
||||||
.suffixedAttachment,
|
|
||||||
value: attachment,
|
|
||||||
range: NSRange(location: index, length: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set highlight
|
// set highlight
|
||||||
var attributes = [NSAttributedString.Key: Any]()
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
||||||
|
|
||||||
// See `traitCollectionDidChange(_:)`
|
// See `traitCollectionDidChange(_:)`
|
||||||
// set accessibility
|
// set accessibility
|
||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
|
@ -319,6 +409,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
attributedString.addAttributes(attributes, range: match.range)
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emoji
|
||||||
let emojis = customEmojiViewModel?.emojis.value ?? []
|
let emojis = customEmojiViewModel?.emojis.value ?? []
|
||||||
if !emojis.isEmpty {
|
if !emojis.isEmpty {
|
||||||
for match in emojiMatches {
|
for match in emojiMatches {
|
||||||
|
@ -366,25 +457,26 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// url
|
||||||
for match in urlMatches {
|
for match in urlMatches {
|
||||||
if let name = string.substring(with: match, at: 0) {
|
guard let name = string.substring(with: match, at: 0) else { continue }
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
||||||
|
|
||||||
// set highlight
|
// set highlight
|
||||||
var attributes = [NSAttributedString.Key: Any]()
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
||||||
// See `traitCollectionDidChange(_:)`
|
|
||||||
// set accessibility
|
// See `traitCollectionDidChange(_:)`
|
||||||
if #available(iOS 13.0, *) {
|
// set accessibility
|
||||||
switch self.traitCollection.accessibilityContrast {
|
if #available(iOS 13.0, *) {
|
||||||
case .high:
|
switch self.traitCollection.accessibilityContrast {
|
||||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
case .high:
|
||||||
default:
|
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||||
break
|
default:
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
attributedString.addAttributes(attributes, range: match.range)
|
|
||||||
}
|
}
|
||||||
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(attributedString)
|
completion(attributedString)
|
||||||
|
@ -394,13 +486,19 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - ComposeToolbarViewDelegate
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, mediaSelectionType.rawValue)
|
||||||
|
switch mediaSelectionType {
|
||||||
|
case .photoLibrary:
|
||||||
|
present(imagePicker, animated: true, completion: nil)
|
||||||
|
case .camera:
|
||||||
|
present(imagePickerController, animated: true, completion: nil)
|
||||||
|
case .browse:
|
||||||
|
present(documentPickerController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
|
||||||
|
@ -422,10 +520,8 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension ComposeViewController: UITableViewDelegate {
|
extension ComposeViewController: UICollectionViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
return UITableView.automaticDimension
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||||
|
@ -446,3 +542,83 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - PHPickerViewControllerDelegate
|
||||||
|
extension ComposeViewController: PHPickerViewControllerDelegate {
|
||||||
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
|
let attachmentServices: [MastodonAttachmentService] = results.map { result in
|
||||||
|
let service = MastodonAttachmentService(
|
||||||
|
context: context,
|
||||||
|
pickerResult: result,
|
||||||
|
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||||
|
)
|
||||||
|
service.delegate = viewModel
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
viewModel.attachmentServices.value = viewModel.attachmentServices.value + attachmentServices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIImagePickerControllerDelegate
|
||||||
|
extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
|
||||||
|
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
|
guard let image = info[.originalImage] as? UIImage else { return }
|
||||||
|
|
||||||
|
let attachmentService = MastodonAttachmentService(
|
||||||
|
context: context,
|
||||||
|
image: image,
|
||||||
|
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||||
|
)
|
||||||
|
attachmentService.delegate = viewModel
|
||||||
|
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIDocumentPickerDelegate
|
||||||
|
extension ComposeViewController: UIDocumentPickerDelegate {
|
||||||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
|
guard let url = urls.first else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else { return }
|
||||||
|
defer { url.stopAccessingSecurityScopedResource() }
|
||||||
|
let imageData = try Data(contentsOf: url)
|
||||||
|
let attachmentService = MastodonAttachmentService(
|
||||||
|
context: context,
|
||||||
|
imageData: imageData,
|
||||||
|
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||||
|
)
|
||||||
|
attachmentService.delegate = viewModel
|
||||||
|
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
||||||
|
} catch {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ComposeStatusAttachmentTableViewCellDelegate
|
||||||
|
extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
|
||||||
|
|
||||||
|
func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let indexPath = collectionView.indexPath(for: cell) else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
guard case let .attachment(attachmentService) = item else { return }
|
||||||
|
|
||||||
|
var attachmentServices = viewModel.attachmentServices.value
|
||||||
|
guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
|
||||||
|
attachmentServices.remove(at: index)
|
||||||
|
viewModel.attachmentServices.value = attachmentServices
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -11,20 +11,44 @@ import TwitterTextEditor
|
||||||
extension ComposeViewModel {
|
extension ComposeViewModel {
|
||||||
|
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
for tableView: UITableView,
|
for collectionView: UICollectionView,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
|
||||||
) {
|
) {
|
||||||
diffableDataSource = ComposeStatusSection.tableViewDiffableDataSource(
|
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
|
||||||
for: tableView,
|
for: collectionView,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
composeKind: composeKind,
|
composeKind: composeKind,
|
||||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||||
|
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Note: do not allow reorder due to the images display order following the upload time
|
||||||
|
// diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
||||||
|
// switch item {
|
||||||
|
// case .attachment: return true
|
||||||
|
// default: return false
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||||
|
// guard let self = self else { return }
|
||||||
|
//
|
||||||
|
// let items = transaction.finalSnapshot.itemIdentifiers
|
||||||
|
// var attachmentServices: [MastodonAttachmentService] = []
|
||||||
|
// for item in items {
|
||||||
|
// guard case let .attachment(attachmentService) = item else { continue }
|
||||||
|
// attachmentServices.append(attachmentService)
|
||||||
|
// }
|
||||||
|
// self.attachmentServices.value = attachmentServices
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
|
self.diffableDataSource = diffableDataSource
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||||
snapshot.appendSections([.repliedTo, .status])
|
snapshot.appendSections([.repliedTo, .status, .attachment])
|
||||||
switch composeKind {
|
switch composeKind {
|
||||||
case .reply(let statusObjectID):
|
case .reply(let statusObjectID):
|
||||||
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
//
|
||||||
|
// ComposeViewModel+PublishState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension ComposeViewModel {
|
||||||
|
class PublishState: GKState {
|
||||||
|
weak var viewModel: ComposeViewModel?
|
||||||
|
|
||||||
|
init(viewModel: ComposeViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeViewModel.PublishState {
|
||||||
|
class Initial: ComposeViewModel.PublishState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Publishing.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Publishing: ComposeViewModel.PublishState {
|
||||||
|
|
||||||
|
var publishingSubscription: AnyCancellable?
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Fail.self || stateClass == Finish.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
guard let mastodonAuthenticationBox = viewModel.activeAuthenticationBox.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain = mastodonAuthenticationBox.domain
|
||||||
|
let attachmentServices = viewModel.attachmentServices.value
|
||||||
|
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
||||||
|
attachmentService.attachment.value?.id
|
||||||
|
}
|
||||||
|
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||||
|
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
||||||
|
for attachmentService in attachmentServices {
|
||||||
|
guard let attachmentID = attachmentService.attachment.value?.id else { continue }
|
||||||
|
let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
guard !description.isEmpty else { continue }
|
||||||
|
let query = Mastodon.API.Media.UpdateMediaQuery(
|
||||||
|
file: nil,
|
||||||
|
thumbnail: nil,
|
||||||
|
description: description,
|
||||||
|
focus: nil
|
||||||
|
)
|
||||||
|
let subscription = viewModel.context.apiService.updateMedia(
|
||||||
|
domain: domain,
|
||||||
|
attachmentID: attachmentID,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: mastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
subscriptions.append(subscription)
|
||||||
|
}
|
||||||
|
return subscriptions
|
||||||
|
}()
|
||||||
|
|
||||||
|
publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions)
|
||||||
|
.collect()
|
||||||
|
.flatMap { attachments -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||||
|
let query = Mastodon.API.Statuses.PublishStatusQuery(
|
||||||
|
status: viewModel.composeStatusAttribute.composeContent.value,
|
||||||
|
mediaIDs: mediaIDs
|
||||||
|
)
|
||||||
|
return viewModel.context.apiService.publishStatus(
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: mastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
case .finished:
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
stateMachine.enter(Finish.self)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: ComposeViewModel.PublishState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// allow discard publishing
|
||||||
|
return stateClass == Publishing.self || stateClass == Finish.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Finish: ComposeViewModel.PublishState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import GameplayKit
|
||||||
|
|
||||||
final class ComposeViewModel {
|
final class ComposeViewModel {
|
||||||
|
|
||||||
|
@ -18,20 +19,34 @@ final class ComposeViewModel {
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let composeKind: ComposeStatusSection.ComposeKind
|
let composeKind: ComposeStatusSection.ComposeKind
|
||||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||||
let composeContent = CurrentValueSubject<String, Never>("")
|
|
||||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||||
|
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
//var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
|
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
|
private(set) lazy var publishStateMachine: GKStateMachine = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
PublishState.Initial(viewModel: self),
|
||||||
|
PublishState.Publishing(viewModel: self),
|
||||||
|
PublishState.Fail(viewModel: self),
|
||||||
|
PublishState.Finish(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(PublishState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
// UI & UX
|
// UI & UX
|
||||||
let title: CurrentValueSubject<String, Never>
|
let title: CurrentValueSubject<String, Never>
|
||||||
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isComposeTootBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
// custom emojis
|
// custom emojis
|
||||||
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||||
|
|
||||||
|
// attachment
|
||||||
|
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
|
@ -44,12 +59,16 @@ final class ComposeViewModel {
|
||||||
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
|
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
|
||||||
}
|
}
|
||||||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||||
|
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
// bind active authentication
|
// bind active authentication
|
||||||
context.authenticationService.activeMastodonAuthentication
|
context.authenticationService.activeMastodonAuthentication
|
||||||
.assign(to: \.value, on: activeAuthentication)
|
.assign(to: \.value, on: activeAuthentication)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.assign(to: \.value, on: activeAuthenticationBox)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind avatar and names
|
// bind avatar and names
|
||||||
activeAuthentication
|
activeAuthentication
|
||||||
|
@ -70,14 +89,30 @@ final class ComposeViewModel {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind compose bar button item UI state
|
// bind compose bar button item UI state
|
||||||
composeStatusAttribute.composeContent
|
let isComposeContentEmpty = composeStatusAttribute.composeContent
|
||||||
.receive(on: DispatchQueue.main)
|
.map { ($0 ?? "").isEmpty }
|
||||||
.map { content in
|
let isComposeContentValid = Just(true).eraseToAnyPublisher()
|
||||||
let content = content?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let isMediaEmpty = attachmentServices
|
||||||
return !content.isEmpty
|
.map { $0.isEmpty }
|
||||||
|
let isMediaUploadAllSuccess = attachmentServices
|
||||||
|
.map { services in
|
||||||
|
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
||||||
}
|
}
|
||||||
.assign(to: \.value, on: isComposeTootBarButtonItemEnabled)
|
Publishers.CombineLatest4(
|
||||||
.store(in: &disposeBag)
|
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||||
|
isComposeContentValid.eraseToAnyPublisher(),
|
||||||
|
isMediaEmpty.eraseToAnyPublisher(),
|
||||||
|
isMediaUploadAllSuccess.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess in
|
||||||
|
if isMediaEmpty {
|
||||||
|
return isComposeContentValid && !isComposeContentEmpty
|
||||||
|
} else {
|
||||||
|
return isComposeContentValid && isMediaUploadAllSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: isPublishBarButtonItemEnabled)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind modal dismiss state
|
// bind modal dismiss state
|
||||||
composeStatusAttribute.composeContent
|
composeStatusAttribute.composeContent
|
||||||
|
@ -101,6 +136,54 @@ final class ComposeViewModel {
|
||||||
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
|
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind snapshot and drive service upload state
|
||||||
|
attachmentServices
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] attachmentServices in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
var snapshot = diffableDataSource.snapshot()
|
||||||
|
|
||||||
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
|
||||||
|
var items: [ComposeStatusItem] = []
|
||||||
|
for attachmentService in attachmentServices {
|
||||||
|
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
|
||||||
|
items.append(item)
|
||||||
|
}
|
||||||
|
snapshot.appendItems(items, toSection: .attachment)
|
||||||
|
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
|
||||||
|
// make image upload in the queue
|
||||||
|
for attachmentService in attachmentServices {
|
||||||
|
// skip when prefix N task when task finish OR fail OR uploading
|
||||||
|
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
|
||||||
|
if currentState is MastodonAttachmentService.UploadState.Fail {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if currentState is MastodonAttachmentService.UploadState.Finish {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if currentState is MastodonAttachmentService.UploadState.Uploading {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// trigger uploading one by one
|
||||||
|
if currentState is MastodonAttachmentService.UploadState.Initial {
|
||||||
|
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - MastodonAttachmentServiceDelegate
|
||||||
|
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||||
|
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
|
||||||
|
// trigger new output event
|
||||||
|
attachmentServices.value = attachmentServices.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
//
|
|
||||||
// ComposeRepliedToTootContentTableViewCell.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class ComposeRepliedToTootContentTableViewCell: UITableViewCell {
|
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ComposeRepliedToTootContentTableViewCell {
|
|
||||||
|
|
||||||
private func _init() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
//
|
||||||
|
// AttachmentContainerView+EmptyStateView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension AttachmentContainerView {
|
||||||
|
final class EmptyStateView: UIView {
|
||||||
|
|
||||||
|
static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
|
||||||
|
static let videoSplashImage: UIImage = {
|
||||||
|
let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
|
||||||
|
return image
|
||||||
|
}()
|
||||||
|
|
||||||
|
let imageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
let label: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .preferredFont(forTextStyle: .body)
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
|
||||||
|
label.numberOfLines = 2
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AttachmentContainerView.EmptyStateView {
|
||||||
|
private func _init() {
|
||||||
|
layer.masksToBounds = true
|
||||||
|
layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||||
|
layer.cornerCurve = .continuous
|
||||||
|
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
let topPaddingView = UIView()
|
||||||
|
let middlePaddingView = UIView()
|
||||||
|
let bottomPaddingView = UIView()
|
||||||
|
|
||||||
|
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(topPaddingView)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh),
|
||||||
|
imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
imageView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
|
middlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(middlePaddingView)
|
||||||
|
stackView.addArrangedSubview(label)
|
||||||
|
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(bottomPaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
|
||||||
|
bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||||
|
])
|
||||||
|
return emptyStateView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 205))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||||
|
])
|
||||||
|
return emptyStateView
|
||||||
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 205))
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||||
|
emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage
|
||||||
|
emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
emptyStateView.heightAnchor.constraint(equalToConstant: 205)
|
||||||
|
])
|
||||||
|
return emptyStateView
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 205))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
//
|
||||||
|
// AttachmentContainerView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UITextView_Placeholder
|
||||||
|
|
||||||
|
final class AttachmentContainerView: UIView {
|
||||||
|
|
||||||
|
static let containerViewCornerRadius: CGFloat = 4
|
||||||
|
|
||||||
|
var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||||
|
|
||||||
|
let previewImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let emptyStateView = AttachmentContainerView.EmptyStateView()
|
||||||
|
let descriptionBackgroundView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.masksToBounds = true
|
||||||
|
view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
|
||||||
|
view.layer.cornerCurve = .continuous
|
||||||
|
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
let descriptionBackgroundGradientLayer: CAGradientLayer = {
|
||||||
|
let gradientLayer = CAGradientLayer()
|
||||||
|
gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor]
|
||||||
|
gradientLayer.locations = [0.0, 1.0]
|
||||||
|
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
|
||||||
|
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
|
||||||
|
gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
|
||||||
|
return gradientLayer
|
||||||
|
}()
|
||||||
|
let descriptionTextView: UITextView = {
|
||||||
|
let textView = UITextView()
|
||||||
|
textView.showsVerticalScrollIndicator = false
|
||||||
|
textView.backgroundColor = .clear
|
||||||
|
textView.textColor = .white
|
||||||
|
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
|
||||||
|
textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
|
||||||
|
textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
|
||||||
|
textView.returnKeyType = .done
|
||||||
|
return textView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AttachmentContainerView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
previewImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(previewImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
previewImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(descriptionBackgroundView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3),
|
||||||
|
])
|
||||||
|
descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer)
|
||||||
|
descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
descriptionBackgroundView.addSubview(descriptionTextView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor),
|
||||||
|
descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor),
|
||||||
|
descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36),
|
||||||
|
])
|
||||||
|
|
||||||
|
emptyStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(emptyStateView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
emptyStateView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(activityIndicatorView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor),
|
||||||
|
activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
emptyStateView.isHidden = true
|
||||||
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
activityIndicatorView.startAnimating()
|
||||||
|
|
||||||
|
descriptionTextView.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension AttachmentContainerView: UITextViewDelegate {
|
||||||
|
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||||
|
// let keyboard dismiss when input description with "done" type return key
|
||||||
|
if textView === descriptionTextView, text == "\n" {
|
||||||
|
textView.resignFirstResponder()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol ComposeToolbarViewDelegate: class {
|
protocol ComposeToolbarViewDelegate: class {
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
|
||||||
|
@ -17,41 +17,42 @@ protocol ComposeToolbarViewDelegate: class {
|
||||||
|
|
||||||
final class ComposeToolbarView: UIView {
|
final class ComposeToolbarView: UIView {
|
||||||
|
|
||||||
|
static let toolbarButtonSize: CGSize = CGSize(width: 44, height: 44)
|
||||||
static let toolbarHeight: CGFloat = 44
|
static let toolbarHeight: CGFloat = 44
|
||||||
|
|
||||||
weak var delegate: ComposeToolbarViewDelegate?
|
weak var delegate: ComposeToolbarViewDelegate?
|
||||||
|
|
||||||
let mediaButton: UIButton = {
|
let mediaButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = HighlightDimmableButton()
|
||||||
button.tintColor = Asset.Colors.Button.normal.color
|
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||||
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let pollButton: UIButton = {
|
let pollButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = HighlightDimmableButton(type: .custom)
|
||||||
button.tintColor = Asset.Colors.Button.normal.color
|
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||||
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
|
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let emojiButton: UIButton = {
|
let emojiButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = HighlightDimmableButton()
|
||||||
button.tintColor = Asset.Colors.Button.normal.color
|
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||||
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let contentWarningButton: UIButton = {
|
let contentWarningButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = HighlightDimmableButton()
|
||||||
button.tintColor = Asset.Colors.Button.normal.color
|
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||||
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let visibilityButton: UIButton = {
|
let visibilityButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = HighlightDimmableButton()
|
||||||
button.tintColor = Asset.Colors.Button.normal.color
|
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||||
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
|
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
@ -99,7 +100,8 @@ extension ComposeToolbarView {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaButton.addTarget(self, action: #selector(ComposeToolbarView.cameraButtonDidPressed(_:)), for: .touchUpInside)
|
mediaButton.menu = createMediaContextMenu()
|
||||||
|
mediaButton.showsMenuAsPrimaryAction = true
|
||||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
|
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
|
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
|
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
@ -107,13 +109,52 @@ extension ComposeToolbarView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ComposeToolbarView {
|
||||||
|
enum MediaSelectionType: String {
|
||||||
|
case camera
|
||||||
|
case photoLibrary
|
||||||
|
case browse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ComposeToolbarView {
|
extension ComposeToolbarView {
|
||||||
|
|
||||||
@objc private func cameraButtonDidPressed(_ sender: UIButton) {
|
private static func configureToolbarButtonAppearance(button: UIButton) {
|
||||||
delegate?.composeToolbarView(self, cameraButtonDidPressed: sender)
|
button.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
button.setBackgroundImage(.placeholder(size: ComposeToolbarView.toolbarButtonSize, color: .systemFill), for: .highlighted)
|
||||||
|
button.layer.masksToBounds = true
|
||||||
|
button.layer.cornerRadius = 5
|
||||||
|
button.layer.cornerCurve = .continuous
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createMediaContextMenu() -> UIMenu {
|
||||||
|
var children: [UIMenuElement] = []
|
||||||
|
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary)
|
||||||
|
}
|
||||||
|
children.append(photoLibraryAction)
|
||||||
|
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||||
|
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .camera)
|
||||||
|
})
|
||||||
|
children.append(cameraAction)
|
||||||
|
}
|
||||||
|
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .browse)
|
||||||
|
}
|
||||||
|
children.append(browseAction)
|
||||||
|
|
||||||
|
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension ComposeToolbarView {
|
||||||
|
|
||||||
@objc private func gifButtonDidPressed(_ sender: UIButton) {
|
@objc private func gifButtonDidPressed(_ sender: UIButton) {
|
||||||
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
|
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ final class HomeTimelineNavigationBarView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static let newPostsView: UIView = {
|
static let newPostsView: UIView = {
|
||||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.highlight.color)
|
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.normal.color)
|
||||||
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.newPosts)
|
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.newPosts)
|
||||||
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
||||||
return view
|
return view
|
||||||
|
|
|
@ -37,10 +37,10 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||||
private(set) lazy var signUpButton: PrimaryActionButton = {
|
private(set) lazy var signUpButton: PrimaryActionButton = {
|
||||||
let button = PrimaryActionButton()
|
let button = PrimaryActionButton()
|
||||||
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
||||||
let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.Button.highlight.color
|
let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.Button.normal.color
|
||||||
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
|
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
|
||||||
button.setBackgroundImage(.placeholder(color: backgroundImageColor.withAlphaComponent(0.9)), for: .highlighted)
|
button.setBackgroundImage(.placeholder(color: backgroundImageColor.withAlphaComponent(0.9)), for: .highlighted)
|
||||||
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.highlight.color : UIColor.white
|
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.normal.color : UIColor.white
|
||||||
button.setTitleColor(titleColor, for: .normal)
|
button.setTitleColor(titleColor, for: .normal)
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return button
|
return button
|
||||||
|
@ -50,7 +50,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
||||||
let button = UIButton(type: .system)
|
let button = UIButton(type: .system)
|
||||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
|
||||||
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||||
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor.white.withAlphaComponent(0.8) : Asset.Colors.Button.highlight.color
|
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor.white.withAlphaComponent(0.8) : Asset.Colors.Button.normal.color
|
||||||
button.setTitleColor(titleColor, for: .normal)
|
button.setTitleColor(titleColor, for: .normal)
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return button
|
return button
|
||||||
|
|
|
@ -9,6 +9,8 @@ import UIKit
|
||||||
|
|
||||||
final class HighlightDimmableButton: UIButton {
|
final class HighlightDimmableButton: UIButton {
|
||||||
|
|
||||||
|
var expandEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -19,6 +21,9 @@ final class HighlightDimmableButton: UIButton {
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
return bounds.inset(by: expandEdgeInsets).contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
override var isHighlighted: Bool {
|
override var isHighlighted: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
|
|
|
@ -38,8 +38,8 @@ extension PrimaryActionButton {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
||||||
setTitleColor(.white, for: .normal)
|
setTitleColor(.white, for: .normal)
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.highlight.color), for: .normal)
|
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.normal.color), for: .normal)
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.highlight.color.withAlphaComponent(0.5)), for: .highlighted)
|
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||||
applyCornerRadius(radius: 10)
|
applyCornerRadius(radius: 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ final class AudioContainerView: UIView {
|
||||||
stackView.isLayoutMarginsRelativeArrangement = true
|
stackView.isLayoutMarginsRelativeArrangement = true
|
||||||
stackView.layer.cornerRadius = AudioContainerView.cornerRadius
|
stackView.layer.cornerRadius = AudioContainerView.cornerRadius
|
||||||
stackView.clipsToBounds = true
|
stackView.clipsToBounds = true
|
||||||
stackView.backgroundColor = Asset.Colors.Button.highlight.color
|
stackView.backgroundColor = Asset.Colors.Background.AudioPlayer.highlight.color
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return stackView
|
return stackView
|
||||||
}()
|
}()
|
||||||
|
@ -31,7 +31,7 @@ final class AudioContainerView: UIView {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
view.layer.cornerRadius = 16
|
view.layer.cornerRadius = 16
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
view.backgroundColor = Asset.Colors.Button.highlight.color
|
view.backgroundColor = Asset.Colors.Background.AudioPlayer.highlight.color
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
@ -109,3 +109,20 @@ extension AudioContainerView {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AudioContainerView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
AudioContainerView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,8 @@ extension PlayerContainerView {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
||||||
mediaTypeIndicotorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor),
|
mediaTypeIndicotorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor),
|
||||||
mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh),
|
mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1),
|
||||||
mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh),
|
mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1),
|
||||||
])
|
])
|
||||||
|
|
||||||
addSubview(contentWarningOverlayView)
|
addSubview(contentWarningOverlayView)
|
||||||
|
@ -84,8 +84,8 @@ extension PlayerContainerView {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor),
|
mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor),
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.rightAnchor.constraint(equalTo: contentWarningOverlayView.rightAnchor),
|
mediaTypeIndicotorViewInContentWarningOverlay.rightAnchor.constraint(equalTo: contentWarningOverlayView.rightAnchor),
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.defaultHigh),
|
mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1),
|
||||||
mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.defaultHigh),
|
mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,8 +167,8 @@ final class StatusView: UIView {
|
||||||
let button = HitTestExpandedButton()
|
let button = HitTestExpandedButton()
|
||||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold))
|
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold))
|
||||||
button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal)
|
button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal)
|
||||||
button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal)
|
button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
|
||||||
button.setTitleColor(Asset.Colors.Button.highlight.color.withAlphaComponent(0.8), for: .highlighted)
|
button.setTitleColor(Asset.Colors.Button.normal.color.withAlphaComponent(0.8), for: .highlighted)
|
||||||
button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled)
|
button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled)
|
||||||
button.isEnabled = false
|
button.isEnabled = false
|
||||||
return button
|
return button
|
||||||
|
|
|
@ -35,7 +35,7 @@ final class PollOptionTableViewCell: UITableViewCell {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
||||||
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||||
imageView.tintColor = Asset.Colors.Button.highlight.color
|
imageView.tintColor = Asset.Colors.Button.normal.color
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// APIService+Media.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func uploadMedia(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Media.UploadMeidaQuery,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
return Mastodon.API.Media.uploadMedia(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMedia(
|
||||||
|
domain: String,
|
||||||
|
attachmentID: Mastodon.Entity.Attachment.ID,
|
||||||
|
query: Mastodon.API.Media.UpdateMediaQuery,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
return Mastodon.API.Media.updateMedia(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
attachmentID: attachmentID,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,43 @@ import MastodonSDK
|
||||||
|
|
||||||
extension APIService {
|
extension APIService {
|
||||||
|
|
||||||
|
func publishStatus(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Statuses.PublishStatusQuery,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
return Mastodon.API.Statuses.publishStatus(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||||
|
return APIService.Persist.persistStatus(
|
||||||
|
managedObjectContext: self.backgroundManagedObjectContext,
|
||||||
|
domain: domain,
|
||||||
|
query: nil,
|
||||||
|
response: response.map { [$0] },
|
||||||
|
persistType: .lookUp,
|
||||||
|
requestMastodonUserID: nil,
|
||||||
|
log: OSLog.api
|
||||||
|
)
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func status(
|
func status(
|
||||||
domain: String,
|
domain: String,
|
||||||
statusID: Mastodon.Entity.Status.ID,
|
statusID: Mastodon.Entity.Status.ID,
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
//
|
||||||
|
// MastodonAttachmentService+UploadState.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import Kingfisher
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension MastodonAttachmentService {
|
||||||
|
class UploadState: GKState {
|
||||||
|
weak var service: MastodonAttachmentService?
|
||||||
|
|
||||||
|
init(service: MastodonAttachmentService) {
|
||||||
|
self.service = service
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||||
|
service?.uploadStateMachineSubject.send(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAttachmentService.UploadState {
|
||||||
|
|
||||||
|
class Initial: MastodonAttachmentService.UploadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
guard service?.authenticationBox != nil else { return false }
|
||||||
|
if stateClass == Initial.self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if service?.imageData.value != nil {
|
||||||
|
return stateClass == Uploading.self
|
||||||
|
} else {
|
||||||
|
return stateClass == Fail.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Uploading: MastodonAttachmentService.UploadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return stateClass == Fail.self || stateClass == Finish.self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
|
guard let service = service, let stateMachine = stateMachine else { return }
|
||||||
|
guard let authenticationBox = service.authenticationBox else { return }
|
||||||
|
guard let imageData = service.imageData.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,
|
||||||
|
thumbnail: nil,
|
||||||
|
description: description,
|
||||||
|
focus: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
service.context.apiService.uploadMedia(
|
||||||
|
domain: authenticationBox.domain,
|
||||||
|
query: query,
|
||||||
|
mastodonAuthenticationBox: authenticationBox
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { completion in
|
||||||
|
switch completion {
|
||||||
|
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)
|
||||||
|
case .finished:
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment %s success: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.url)
|
||||||
|
service.attachment.value = response.value
|
||||||
|
stateMachine.enter(Finish.self)
|
||||||
|
}
|
||||||
|
.store(in: &service.disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: MastodonAttachmentService.UploadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
// allow discard publishing
|
||||||
|
return stateClass == Uploading.self || stateClass == Finish.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Finish: MastodonAttachmentService.UploadState {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
//
|
||||||
|
// MastodonAttachmentService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-17.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import PhotosUI
|
||||||
|
import Kingfisher
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
protocol MastodonAttachmentServiceDelegate: class {
|
||||||
|
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MastodonAttachmentService {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
weak var delegate: MastodonAttachmentServiceDelegate?
|
||||||
|
|
||||||
|
let identifier = UUID()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
var authenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||||
|
|
||||||
|
// output
|
||||||
|
// TODO: handle video/GIF/Audio data
|
||||||
|
let imageData = CurrentValueSubject<Data?, 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 = {
|
||||||
|
// exclude timeline middle fetcher state
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
UploadState.Initial(service: self),
|
||||||
|
UploadState.Uploading(service: self),
|
||||||
|
UploadState.Fail(service: self),
|
||||||
|
UploadState.Finish(service: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(UploadState.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
lazy var uploadStateMachineSubject = CurrentValueSubject<MastodonAttachmentService.UploadState?, Never>(nil)
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AppContext,
|
||||||
|
pickerResult: PHPickerResult,
|
||||||
|
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.authenticationBox = initalAuthenticationBox
|
||||||
|
// end init
|
||||||
|
|
||||||
|
setupServiceObserver()
|
||||||
|
|
||||||
|
PHPickerResultLoader.loadImageData(from: pickerResult)
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
self.error.value = error
|
||||||
|
self.uploadStateMachine.enter(UploadState.Fail.self)
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] imageData in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.imageData.value = imageData
|
||||||
|
self.uploadStateMachine.enter(UploadState.Initial.self)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AppContext,
|
||||||
|
image: UIImage,
|
||||||
|
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.authenticationBox = initalAuthenticationBox
|
||||||
|
// end init
|
||||||
|
|
||||||
|
setupServiceObserver()
|
||||||
|
|
||||||
|
imageData.value = image.jpegData(compressionQuality: 0.75)
|
||||||
|
uploadStateMachine.enter(UploadState.Initial.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
context: AppContext,
|
||||||
|
imageData: Data,
|
||||||
|
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.authenticationBox = initalAuthenticationBox
|
||||||
|
// end init
|
||||||
|
|
||||||
|
setupServiceObserver()
|
||||||
|
|
||||||
|
self.imageData.value = imageData
|
||||||
|
uploadStateMachine.enter(UploadState.Initial.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupServiceObserver() {
|
||||||
|
uploadStateMachineSubject
|
||||||
|
.sink { [weak self] state in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.mastodonAttachmentService(self, uploadStateDidChange: state)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAttachmentService {
|
||||||
|
// FIXME: needs reset state for multiple account posting support
|
||||||
|
func uploading(mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> Bool {
|
||||||
|
authenticationBox = mastodonAuthenticationBox
|
||||||
|
return uploadStateMachine.enter(UploadState.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAttachmentService: Equatable, Hashable {
|
||||||
|
|
||||||
|
static func == (lhs: MastodonAttachmentService, rhs: MastodonAttachmentService) -> Bool {
|
||||||
|
return lhs.identifier == rhs.identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
//
|
||||||
|
// PHPickerResultLoader.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import MobileCoreServices
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
|
// 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> {
|
||||||
|
Future { promise in
|
||||||
|
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in
|
||||||
|
if let error = error {
|
||||||
|
promise(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = url else {
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
||||||
|
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let downsampleOptions = [
|
||||||
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||||
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||||
|
kCGImageSourceThumbnailMaxPixelSize: 4096,
|
||||||
|
] as CFDictionary
|
||||||
|
|
||||||
|
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = NSMutableData()
|
||||||
|
guard let imageDestination = CGImageDestinationCreateWithData(data, kUTTypeJPEG, 1, nil) else {
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPNG: Bool = {
|
||||||
|
guard let utType = cgImage.utType else { return false }
|
||||||
|
return (utType as String) == UTType.png.identifier
|
||||||
|
}()
|
||||||
|
|
||||||
|
let destinationProperties = [
|
||||||
|
kCGImageDestinationLossyCompressionQuality: isPNG ? 1.0 : 0.75
|
||||||
|
] as CFDictionary
|
||||||
|
|
||||||
|
CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
|
||||||
|
CGImageDestinationFinalize(imageDestination)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -198,6 +198,10 @@ extension Mastodon.API.Account {
|
||||||
return Self.multipartContentType()
|
return Self.multipartContentType()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queryItems: [URLQueryItem]? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var body: Data? {
|
var body: Data? {
|
||||||
var data = Data()
|
var data = Data()
|
||||||
|
|
||||||
|
@ -219,6 +223,7 @@ extension Mastodon.API.Account {
|
||||||
data.append(Data.multipart(key: "fields_attributes[value][]", value: fieldsAttribute.value))
|
data.append(Data.multipart(key: "fields_attributes[value][]", value: fieldsAttribute.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.append(Data.multipartEnd())
|
data.append(Data.multipartEnd())
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
//
|
||||||
|
// Mastodon+API+Media.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension Mastodon.API.Media {
|
||||||
|
|
||||||
|
static func uploadMediaEndpointURL(domain: String) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload media as attachment
|
||||||
|
///
|
||||||
|
/// Creates an attachment to be used with a new status.
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/18
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/media/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - query: `UploadMediaQuery`
|
||||||
|
/// - authorization: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains `Attachment` nested in the response
|
||||||
|
public static func uploadMedia(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
query: UploadMeidaQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||||
|
var request = Mastodon.API.post(
|
||||||
|
url: uploadMediaEndpointURL(domain: domain),
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct UploadMeidaQuery: PostQuery, 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 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)) }
|
||||||
|
|
||||||
|
data.append(Data.multipartEnd())
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Media {
|
||||||
|
|
||||||
|
static func updateMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media").appendingPathComponent(attachmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update attachment
|
||||||
|
///
|
||||||
|
/// Update an Attachment, before it is attached to a status and posted..
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/18
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/media/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - query: `UploadMediaQuery`
|
||||||
|
/// - authorization: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains `Attachment` nested in the response
|
||||||
|
public static func updateMedia(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
attachmentID: Mastodon.Entity.Attachment.ID,
|
||||||
|
query: UpdateMediaQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error> {
|
||||||
|
var request = Mastodon.API.put(
|
||||||
|
url: updateMediaEndpointURL(domain: domain, attachmentID: attachmentID),
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias UpdateMediaQuery = UploadMeidaQuery
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -51,3 +51,61 @@ extension Mastodon.API.Statuses {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Mastodon.API.Statuses {
|
||||||
|
|
||||||
|
static func publishNewStatusEndpointURL(domain: String) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("statuses")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish new status
|
||||||
|
///
|
||||||
|
/// Post a new status.
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.0
|
||||||
|
/// # Last Update
|
||||||
|
/// 2021/3/18
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - query: `PublishStatusQuery`
|
||||||
|
/// - authorization: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains `Status` nested in the response
|
||||||
|
public static func publishStatus(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
query: PublishStatusQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||||
|
let request = Mastodon.API.post(
|
||||||
|
url: publishNewStatusEndpointURL(domain: domain),
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PublishStatusQuery: Codable, PostQuery {
|
||||||
|
public let status: String?
|
||||||
|
public let mediaIDs: [String]?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case status
|
||||||
|
case mediaIDs = "media_ids"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(status: String?, mediaIDs: [String]?) {
|
||||||
|
self.status = status
|
||||||
|
self.mediaIDs = mediaIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ extension Mastodon.API {
|
||||||
public enum CustomEmojis { }
|
public enum CustomEmojis { }
|
||||||
public enum Favorites { }
|
public enum Favorites { }
|
||||||
public enum Instance { }
|
public enum Instance { }
|
||||||
|
public enum Media { }
|
||||||
public enum OAuth { }
|
public enum OAuth { }
|
||||||
public enum Onboarding { }
|
public enum Onboarding { }
|
||||||
public enum Polls { }
|
public enum Polls { }
|
||||||
|
@ -128,6 +129,14 @@ extension Mastodon.API {
|
||||||
return buildRequest(url: url, method: .PATCH, query: query, authorization: authorization)
|
return buildRequest(url: url, method: .PATCH, query: query, authorization: authorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func put(
|
||||||
|
url: URL,
|
||||||
|
query: PutQuery?,
|
||||||
|
authorization: OAuth.Authorization?
|
||||||
|
) -> URLRequest {
|
||||||
|
return buildRequest(url: url, method: .PUT, query: query, authorization: authorization)
|
||||||
|
}
|
||||||
|
|
||||||
private static func buildRequest(
|
private static func buildRequest(
|
||||||
url: URL,
|
url: URL,
|
||||||
method: RequestMethod,
|
method: RequestMethod,
|
||||||
|
|
|
@ -35,6 +35,7 @@ extension RequestQuery where Self: Encodable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET
|
||||||
protocol GetQuery: RequestQuery { }
|
protocol GetQuery: RequestQuery { }
|
||||||
|
|
||||||
extension GetQuery {
|
extension GetQuery {
|
||||||
|
@ -43,6 +44,7 @@ extension GetQuery {
|
||||||
var contentType: String? { nil }
|
var contentType: String? { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST
|
||||||
protocol PostQuery: RequestQuery { }
|
protocol PostQuery: RequestQuery { }
|
||||||
|
|
||||||
extension PostQuery {
|
extension PostQuery {
|
||||||
|
@ -50,10 +52,9 @@ extension PostQuery {
|
||||||
var queryItems: [URLQueryItem]? { nil }
|
var queryItems: [URLQueryItem]? { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PATCH
|
||||||
protocol PatchQuery: RequestQuery { }
|
protocol PatchQuery: RequestQuery { }
|
||||||
|
|
||||||
extension PatchQuery {
|
// PUT
|
||||||
// By default a `PatchQuery` does not has query items
|
protocol PutQuery: RequestQuery { }
|
||||||
var queryItems: [URLQueryItem]? { nil }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -54,5 +54,6 @@ arch -x86_64 pod install
|
||||||
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
||||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||||
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
|
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
|
||||||
|
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
Loading…
Reference in New Issue