feat: implement take photo and browser for image for compose scene

This commit is contained in:
CMK 2021-03-19 19:49:48 +08:00
parent 75d10b76c8
commit 36b42ba3e7
16 changed files with 375 additions and 65 deletions

View File

@ -193,6 +193,11 @@
"new_post": "New Post",
"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",
"compose_action": "Publish",
"attachment": {

View File

@ -85,19 +85,32 @@ extension ComposeStatusSection {
cell.attachmentContainerView.previewImageView.image = placeholder
return
}
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
cell.attachmentContainerView.previewImageView.image = image
.af.imageAspectScaled(toFill: cell.attachmentContainerView.previewImageView.frame.size)
.af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius)
}
.store(in: &cell.disposeBag)
attachmentService.error
.receive(on: DispatchQueue.main)
.sink { error in
Publishers.CombineLatest(
attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
attachmentService.error.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { uploadState, error in
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
if let _ = error {
cell.attachmentContainerView.activityIndicatorView.stopAnimating()
cell.attachmentContainerView.emptyStateView.isHidden = error == nil
} 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)
}
.store(in: &cell.disposeBag)
NotificationCenter.default.publisher(
for: UITextView.textDidChangeNotification,
object: cell.attachmentContainerView.descriptionTextView

View File

@ -158,6 +158,14 @@ internal enum L10n {
/// 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 {
/// New Post
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")

View File

@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x84",
"red" : "0x0A"
}
},
"idiom" : "universal"
}
],
"info" : {

View File

@ -46,6 +46,9 @@ uploaded to Mastodon.";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.ComposeAction" = "Publish";
"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.NewReply" = "New Reply";
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";

View File

@ -62,7 +62,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
return backgroundView
}()
lazy var imagePicker: PHPickerViewController = {
private(set) lazy var imagePicker: PHPickerViewController = {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 4
@ -71,6 +71,18 @@ final class ComposeViewController: UIViewController, NeedsDependency {
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
}()
}
@ -433,9 +445,16 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
// MARK: - ComposeToolbarViewDelegate
extension ComposeViewController: ComposeToolbarViewDelegate {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
present(imagePicker, animated: true, completion: nil)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
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) {
@ -500,6 +519,51 @@ extension ComposeViewController: PHPickerViewControllerDelegate {
}
}
// 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: ComposeStatusAttachmentTableViewCellDelegate {

View File

@ -48,31 +48,60 @@ extension ComposeViewModel.PublishState {
return
}
let mediaIDs = viewModel.attachmentServices.value.compactMap { attachmentService in
let domain = mastodonAuthenticationBox.domain
let attachmentServices = viewModel.attachmentServices.value
let mediaIDs = attachmentServices.compactMap { attachmentService in
attachmentService.attachment.value?.id
}
let query = Mastodon.API.Statuses.PublishStatusQuery(
status: viewModel.composeStatusAttribute.composeContent.value,
mediaIDs: mediaIDs
)
publishingSubscription = viewModel.context.apiService.publishStatus(
domain: mastodonAuthenticationBox.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)
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)
}
} 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)
}
}
}

View File

@ -50,6 +50,7 @@ extension AttachmentContainerView.EmptyStateView {
layer.masksToBounds = true
layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
layer.cornerCurve = .continuous
backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
let stackView = UIStackView()
stackView.axis = .vertical

View File

@ -14,7 +14,7 @@ final class AttachmentContainerView: UIView {
var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
let activityIndicatorView = UIActivityIndicatorView(style: .large)
let previewImageView: UIImageView = {
let imageView = UIImageView()
@ -49,7 +49,8 @@ final class AttachmentContainerView: UIView {
textView.textColor = .white
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
textView.placeholderColor = Asset.Colors.Label.secondary.color
textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
textView.returnKeyType = .done
return textView
}()
@ -115,12 +116,25 @@ extension AttachmentContainerView {
activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor),
activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor),
])
descriptionBackgroundView.overrideUserInterfaceStyle = .dark
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
}
}

View File

@ -8,7 +8,7 @@
import UIKit
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, atButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
@ -17,41 +17,42 @@ protocol ComposeToolbarViewDelegate: class {
final class ComposeToolbarView: UIView {
static let toolbarButtonSize: CGSize = CGSize(width: 44, height: 44)
static let toolbarHeight: CGFloat = 44
weak var delegate: ComposeToolbarViewDelegate?
let mediaButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let pollButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
let button = HighlightDimmableButton(type: .custom)
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
return button
}()
let emojiButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let contentWarningButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let visibilityButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
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)
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), 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 {
@objc private func cameraButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, cameraButtonDidPressed: sender)
private static func configureToolbarButtonAppearance(button: UIButton) {
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) {
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
}

View File

@ -26,4 +26,21 @@ extension APIService {
)
}
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
)
}
}

View File

@ -25,7 +25,6 @@ final class MastodonAttachmentService {
// input
let context: AppContext
let pickerResult: PHPickerResult
var authenticationBox: AuthenticationService.MastodonAuthenticationBox?
// output
@ -54,16 +53,10 @@ final class MastodonAttachmentService {
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
) {
self.context = context
self.pickerResult = pickerResult
self.authenticationBox = initalAuthenticationBox
// end init
uploadStateMachineSubject
.sink { [weak self] state in
guard let self = self else { return }
self.delegate?.mastodonAttachmentService(self, uploadStateDidChange: state)
}
.store(in: &disposeBag)
setupServiceObserver()
PHPickerResultLoader.loadImageData(from: pickerResult)
.sink { [weak self] completion in
@ -84,6 +77,49 @@ final class MastodonAttachmentService {
.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)
// Try pre-upload attachment for current active user
uploadStateMachine.enter(UploadState.Uploading.self)
}
init(
context: AppContext,
imageData: Data,
initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox?
) {
self.context = context
self.authenticationBox = initalAuthenticationBox
// end init
setupServiceObserver()
self.imageData.value = imageData
// Try pre-upload attachment for current active user
uploadStateMachine.enter(UploadState.Uploading.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 {

View File

@ -198,6 +198,10 @@ extension Mastodon.API.Account {
return Self.multipartContentType()
}
var queryItems: [URLQueryItem]? {
return nil
}
var body: Data? {
var data = Data()

View File

@ -50,7 +50,7 @@ extension Mastodon.API.Media {
.eraseToAnyPublisher()
}
public struct UploadMeidaQuery: PostQuery {
public struct UploadMeidaQuery: PostQuery, PutQuery {
public let file: Mastodon.Query.MediaAttachment?
public let thumbnail: Mastodon.Query.MediaAttachment?
public let description: String?
@ -86,3 +86,51 @@ extension Mastodon.API.Media {
}
}
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
}

View File

@ -128,6 +128,14 @@ extension Mastodon.API {
) -> URLRequest {
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(
url: URL,

View File

@ -35,6 +35,7 @@ extension RequestQuery where Self: Encodable {
}
}
// GET
protocol GetQuery: RequestQuery { }
extension GetQuery {
@ -43,6 +44,7 @@ extension GetQuery {
var contentType: String? { nil }
}
// POST
protocol PostQuery: RequestQuery { }
extension PostQuery {
@ -50,10 +52,9 @@ extension PostQuery {
var queryItems: [URLQueryItem]? { nil }
}
// PATCH
protocol PatchQuery: RequestQuery { }
extension PatchQuery {
// By default a `PatchQuery` does not has query items
var queryItems: [URLQueryItem]? { nil }
}
// PUT
protocol PutQuery: RequestQuery { }