forked from zelo72/mastodon-ios
feat: implement take photo and browser for image for compose scene
This commit is contained in:
parent
75d10b76c8
commit
36b42ba3e7
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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" : {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -198,6 +198,10 @@ extension Mastodon.API.Account {
|
|||
return Self.multipartContentType()
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var body: Data? {
|
||||
var data = Data()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 { }
|
||||
|
||||
|
|
Loading…
Reference in New Issue