From ca25d43f4f2b7ce05d0bc18a49668d4a541584dc Mon Sep 17 00:00:00 2001
From: sunxiaojian <sunxiaojian@sujitech.im>
Date: Mon, 29 Mar 2021 13:37:56 +0800
Subject: [PATCH] feature: Add context menu for avatar select for sign up page

---
 Localization/app.json                         |   5 +-
 Mastodon/Generated/Strings.swift              |   4 +
 .../Resources/en.lproj/Localizable.strings    |   1 +
 ...astodonRegisterViewController+Avatar.swift | 108 +++++++++++++++---
 .../MastodonRegisterViewController.swift      |  25 +++-
 5 files changed, 118 insertions(+), 25 deletions(-)

diff --git a/Localization/app.json b/Localization/app.json
index c0a305d96..43cc8f6db 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -103,6 +103,9 @@
         "register": {
             "title": "Tell us about you.",
             "input": {
+                "avatar": {
+                    "delete": "delete"
+                },
                 "username": {
                     "placeholder": "username",
                     "duplicate_prompt": "This username is taken."
@@ -228,4 +231,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift
index a875994ea..fe8e6294e 100644
--- a/Mastodon/Generated/Strings.swift
+++ b/Mastodon/Generated/Strings.swift
@@ -331,6 +331,10 @@ internal enum L10n {
         }
       }
       internal enum Input {
+        internal enum Avatar {
+          /// delete
+          internal static let delete = L10n.tr("Localizable", "Scene.Register.Input.Avatar.Delete")
+        }
         internal enum DisplayName {
           /// display name
           internal static let placeholder = L10n.tr("Localizable", "Scene.Register.Input.DisplayName.Placeholder")
diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings
index d2ebb4071..8c93414aa 100644
--- a/Mastodon/Resources/en.lproj/Localizable.strings
+++ b/Mastodon/Resources/en.lproj/Localizable.strings
@@ -102,6 +102,7 @@ tap the link to confirm your account.";
 "Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
 "Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
 "Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
+"Scene.Register.Input.Avatar.Delete" = "delete";
 "Scene.Register.Input.DisplayName.Placeholder" = "display name";
 "Scene.Register.Input.Email.Placeholder" = "email";
 "Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift
index a23585271..3a25fad74 100644
--- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift
+++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift
@@ -7,10 +7,58 @@
 
 import CropViewController
 import Foundation
+import OSLog
 import PhotosUI
 import UIKit
 
+extension MastodonRegisterViewController {
+    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.present(self.imagePicker, animated: true, completion: nil)
+        }
+        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.present(self.imagePickerController, animated: true, completion: nil)
+            })
+            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.present(self.documentPickerController, animated: true, completion: nil)
+        }
+        children.append(browseAction)
+        if self.viewModel.avatarImage.value != nil {
+            let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
+                guard let self = self else { return }
+                self.viewModel.avatarImage.value = nil
+                self.avatarButton.setImage(nil, for: .normal)
+            }
+            children.append(deleteAction)
+        }
+
+        return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
+    }
+    
+    private func cropImage(image:UIImage,pickerViewController:UIViewController) {
+        DispatchQueue.main.async {
+            let cropController = CropViewController(croppingStyle: .default, image: image)
+            cropController.delegate = self
+            cropController.setAspectRatioPreset(.presetSquare, animated: true)
+            cropController.aspectRatioPickerButtonHidden = true
+            cropController.aspectRatioLockEnabled = true
+            pickerViewController.dismiss(animated: true, completion: {
+                self.present(cropController, animated: true, completion: nil)
+            })
+        }
+    }
+}
+
 // MARK: - PHPickerViewControllerDelegate
+
 extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
     func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
         guard let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) else {
@@ -20,11 +68,11 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
         itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
             guard let self = self else { return }
             guard let image = image as? UIImage else {
-                guard let error = error else { return }
-                let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert)
-                let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
-                alertController.addAction(okAction)
                 DispatchQueue.main.async {
+                    guard let error = error else { return }
+                    let alertController = UIAlertController(for: error, title: "", preferredStyle: .alert)
+                    let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
+                    alertController.addAction(okAction)
                     self.coordinator.present(
                         scene: .alertController(alertController: alertController),
                         from: nil,
@@ -33,21 +81,48 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate {
                 }
                 return
             }
-            DispatchQueue.main.async {
-                let cropController = CropViewController(croppingStyle: .default, image: image)
-                cropController.delegate = self
-                cropController.setAspectRatioPreset(.presetSquare, animated: true)
-                cropController.aspectRatioPickerButtonHidden = true
-                cropController.aspectRatioLockEnabled = true
-                picker.dismiss(animated: true, completion: {
-                    self.present(cropController, animated: true, completion: nil)
-                })
-            }
+            self.cropImage(image: image, pickerViewController: picker)
+        }
+    }
+}
+
+// MARK: - UIImagePickerControllerDelegate
+
+extension MastodonRegisterViewController: 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 }
+
+        cropImage(image: image, pickerViewController: picker)
+    }
+
+    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 MastodonRegisterViewController: 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)
+            guard let image = UIImage(data: imageData) else { return }
+            cropImage(image: image, pickerViewController: controller)
+        } catch {
+            os_log("%{public}s[%{public}ld], %{public}s: %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
         }
     }
 }
 
 // MARK: - CropViewControllerDelegate
+
 extension MastodonRegisterViewController: CropViewControllerDelegate {
     public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
         self.viewModel.avatarImage.value = image
@@ -56,8 +131,3 @@ extension MastodonRegisterViewController: CropViewControllerDelegate {
     }
 }
 
-extension MastodonRegisterViewController {
-    @objc func avatarButtonPressed(_ sender: UIButton) {
-        self.present(imagePicker, animated: true, completion: nil)
-    }
-}
diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
index 04aea3c19..8f0162cd3 100644
--- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
+++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
@@ -20,14 +20,28 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
     
     var viewModel: MastodonRegisterViewModel!
 
-    lazy var imagePicker: PHPickerViewController = {
+    // picker
+    private(set) lazy var imagePicker: PHPickerViewController = {
         var configuration = PHPickerConfiguration()
         configuration.filter = .images
+        configuration.selectionLimit = 1
 
         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
+    }()
     
     let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
     
@@ -56,7 +70,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
     }()
     
     let avatarButton: UIButton = {
-        let button = UIButton(type: .custom)
+        let button = HighlightDimmableButton()
         let boldFont = UIFont.systemFont(ofSize: 42)
         let configuration = UIImage.SymbolConfiguration(font: boldFont)
         let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration)
@@ -227,6 +241,9 @@ extension MastodonRegisterViewController {
         setupOnboardingAppearance()
         defer { setupNavigationBarBackgroundView() }
         
+        avatarButton.menu = createMediaContextMenu()
+        avatarButton.showsMenuAsPrimaryAction = true
+        
         domainLabel.text = "@" + viewModel.domain + "  "
         domainLabel.sizeToFit()
         passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: .empty)
@@ -388,9 +405,8 @@ extension MastodonRegisterViewController {
             .receive(on: DispatchQueue.main)
             .sink { [weak self] isHighlighted in
                 guard let self = self else { return }
-                let alpha: CGFloat = isHighlighted ? 0.8 : 1
+                let alpha: CGFloat = isHighlighted ? 0.6 : 1
                 self.plusIconImageView.alpha = alpha
-                self.avatarButton.alpha = alpha
             }
             .store(in: &disposeBag)
 
@@ -550,7 +566,6 @@ extension MastodonRegisterViewController {
                 .store(in: &disposeBag)
         }
         
-        avatarButton.addTarget(self, action: #selector(MastodonRegisterViewController.avatarButtonPressed(_:)), for: .touchUpInside)
         signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside)
     }