mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
dfdf843f66
Fixed an issue where tapping a remote notification wasn't opening the conversation Fixed an issue where the Share Extension could fail due to not having a snode pool loaded Fixed an issue where the Theme may not be applied at the right time in the Share extension
788 lines
28 KiB
Swift
788 lines
28 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import Combine
|
|
import Photos
|
|
import SignalUtilitiesKit
|
|
import SignalCoreKit
|
|
import SessionUIKit
|
|
import SessionUtilitiesKit
|
|
|
|
class SendMediaNavigationController: UINavigationController {
|
|
public override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
return ThemeManager.currentTheme.statusBarStyle
|
|
}
|
|
|
|
// This is a sensitive constant, if you change it make sure to check
|
|
// on iPhone5, 6, 6+, X, layouts.
|
|
static let bottomButtonsCenterOffset: CGFloat = -50
|
|
|
|
private let threadId: String
|
|
private let threadVariant: SessionThread.Variant
|
|
private var disposables: Set<AnyCancellable> = Set()
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(threadId: String, threadVariant: SessionThread.Variant) {
|
|
self.threadId = threadId
|
|
self.threadVariant = threadVariant
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Overrides
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.delegate = self
|
|
|
|
let bottomButtonsCenterOffset = SendMediaNavigationController.bottomButtonsCenterOffset
|
|
|
|
view.addSubview(batchModeButton)
|
|
batchModeButton.setCompressionResistanceHigh()
|
|
batchModeButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
|
batchModeButton.centerXAnchor
|
|
.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: -20)
|
|
.isActive = true
|
|
|
|
view.addSubview(doneButton)
|
|
doneButton.setCompressionResistanceHigh()
|
|
doneButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
|
doneButton.autoPinEdge(toSuperviewMargin: .trailing)
|
|
|
|
view.addSubview(cameraModeButton)
|
|
cameraModeButton.setCompressionResistanceHigh()
|
|
cameraModeButton.centerYAnchor
|
|
.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset)
|
|
.isActive = true
|
|
cameraModeButton.centerXAnchor
|
|
.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 20)
|
|
.isActive = true
|
|
|
|
view.addSubview(mediaLibraryModeButton)
|
|
mediaLibraryModeButton.setCompressionResistanceHigh()
|
|
mediaLibraryModeButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true
|
|
mediaLibraryModeButton.centerXAnchor
|
|
.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 20)
|
|
.isActive = true
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public weak var sendMediaNavDelegate: SendMediaNavDelegate?
|
|
|
|
public class func showingCameraFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController {
|
|
let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant)
|
|
navController.viewControllers = [navController.captureViewController]
|
|
|
|
return navController
|
|
}
|
|
|
|
public class func showingMediaLibraryFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController {
|
|
let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant)
|
|
navController.viewControllers = [navController.mediaLibraryViewController]
|
|
|
|
return navController
|
|
}
|
|
|
|
var isInBatchSelectMode = false {
|
|
didSet {
|
|
if oldValue != isInBatchSelectMode {
|
|
mediaLibraryViewController.batchSelectModeDidChange()
|
|
|
|
guard let topViewController = viewControllers.last else { return }
|
|
|
|
updateButtons(topViewController: topViewController)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateButtons(topViewController: UIViewController) {
|
|
switch topViewController {
|
|
case is AttachmentApprovalViewController:
|
|
batchModeButton.isHidden = true
|
|
doneButton.isHidden = true
|
|
cameraModeButton.isHidden = true
|
|
mediaLibraryModeButton.isHidden = true
|
|
|
|
case is ImagePickerGridController:
|
|
batchModeButton.isHidden = isInBatchSelectMode
|
|
doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0)
|
|
cameraModeButton.isHidden = false
|
|
mediaLibraryModeButton.isHidden = true
|
|
|
|
case is PhotoCaptureViewController:
|
|
batchModeButton.isHidden = isInBatchSelectMode
|
|
doneButton.isHidden = !isInBatchSelectMode || (attachmentDraftCollection.count == 0 && mediaLibrarySelections.count == 0)
|
|
cameraModeButton.isHidden = true
|
|
mediaLibraryModeButton.isHidden = false
|
|
|
|
default:
|
|
owsFailDebug("unexpected topViewController: \(topViewController)")
|
|
}
|
|
|
|
doneButton.updateCount()
|
|
}
|
|
|
|
func fadeTo(viewControllers: [UIViewController]) {
|
|
let transition: CATransition = CATransition()
|
|
transition.duration = 0.1
|
|
transition.type = CATransitionType.fade
|
|
view.layer.add(transition, forKey: nil)
|
|
setViewControllers(viewControllers, animated: false)
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
private func didTapBatchModeButton() {
|
|
// There's no way to _disable_ batch mode.
|
|
isInBatchSelectMode = true
|
|
}
|
|
|
|
private func didTapCameraModeButton() {
|
|
Permissions.requestCameraPermissionIfNeeded { [weak self] in
|
|
DispatchQueue.main.async {
|
|
self?.fadeTo(viewControllers: ((self?.captureViewController).map { [$0] } ?? []))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func didTapMediaLibraryModeButton() {
|
|
Permissions.requestLibraryPermissionIfNeeded { [weak self] in
|
|
DispatchQueue.main.async {
|
|
self?.fadeTo(viewControllers: ((self?.mediaLibraryViewController).map { [$0] } ?? []))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Views
|
|
public static let bottomButtonWidth: CGFloat = 44
|
|
|
|
private lazy var doneButton: DoneButton = {
|
|
let button = DoneButton()
|
|
button.delegate = self
|
|
|
|
return button
|
|
}()
|
|
|
|
private lazy var batchModeButton: InputViewButton = {
|
|
let result: InputViewButton = InputViewButton(
|
|
icon: UIImage(named: "media_send_batch_mode_disabled")?
|
|
.withRenderingMode(.alwaysTemplate)
|
|
) { [weak self] in self?.didTapBatchModeButton() }
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var cameraModeButton: InputViewButton = {
|
|
let result: InputViewButton = InputViewButton(
|
|
icon: UIImage(named: "settings-avatar-camera-2")?
|
|
.withRenderingMode(.alwaysTemplate)
|
|
) { [weak self] in self?.didTapCameraModeButton() }
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var mediaLibraryModeButton: InputViewButton = {
|
|
let result: InputViewButton = InputViewButton(
|
|
icon: UIImage(named: "actionsheet_camera_roll_black")?
|
|
.withRenderingMode(.alwaysTemplate)
|
|
) { [weak self] in self?.didTapMediaLibraryModeButton() }
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: State
|
|
|
|
private lazy var attachmentDraftCollection = AttachmentDraftCollection.empty // Lazy to avoid https://bugs.swift.org/browse/SR-6657
|
|
|
|
private var attachments: [SignalAttachment] {
|
|
return attachmentDraftCollection.attachmentDrafts.map { $0.attachment }
|
|
}
|
|
|
|
private lazy var mediaLibrarySelections = OrderedDictionary<PHAsset, MediaLibrarySelection>() // Lazy to avoid https://bugs.swift.org/browse/SR-6657
|
|
|
|
// MARK: Child VC's
|
|
|
|
private lazy var captureViewController: PhotoCaptureViewController = {
|
|
let vc = PhotoCaptureViewController()
|
|
vc.delegate = self
|
|
|
|
return vc
|
|
}()
|
|
|
|
private lazy var mediaLibraryViewController: ImagePickerGridController = {
|
|
let vc = ImagePickerGridController()
|
|
vc.delegate = self
|
|
vc.collectionView.accessibilityLabel = "Images"
|
|
|
|
return vc
|
|
}()
|
|
|
|
private func pushApprovalViewController() {
|
|
guard let sendMediaNavDelegate = self.sendMediaNavDelegate else {
|
|
owsFailDebug("sendMediaNavDelegate was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
let approvalViewController = AttachmentApprovalViewController(
|
|
mode: .sharedNavigation,
|
|
threadId: self.threadId,
|
|
threadVariant: self.threadVariant,
|
|
attachments: self.attachments
|
|
)
|
|
approvalViewController.approvalDelegate = self
|
|
approvalViewController.messageText = sendMediaNavDelegate.sendMediaNavInitialMessageText(self)
|
|
|
|
pushViewController(approvalViewController, animated: true)
|
|
}
|
|
|
|
private func didRequestExit() {
|
|
guard attachmentDraftCollection.count > 0 else {
|
|
self.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
|
|
return
|
|
}
|
|
|
|
let modal: ConfirmationModal = ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "SEND_MEDIA_ABANDON_TITLE".localized(),
|
|
confirmTitle: "SEND_MEDIA_CONFIRM_ABANDON_ALBUM".localized(),
|
|
confirmStyle: .danger,
|
|
cancelStyle: .alert_text,
|
|
onConfirm: { [weak self] _ in
|
|
self?.sendMediaNavDelegate?.sendMediaNavDidCancel(self)
|
|
}
|
|
)
|
|
)
|
|
self.present(modal, animated: true)
|
|
}
|
|
}
|
|
|
|
extension SendMediaNavigationController: UINavigationControllerDelegate {
|
|
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
|
switch viewController {
|
|
case is PhotoCaptureViewController:
|
|
if attachmentDraftCollection.count == 1 && !isInBatchSelectMode {
|
|
// User is navigating "back" to the previous view, indicating
|
|
// they want to discard the previously captured item
|
|
discardDraft()
|
|
}
|
|
|
|
case is ImagePickerGridController:
|
|
if attachmentDraftCollection.count == 1 && !isInBatchSelectMode {
|
|
isInBatchSelectMode = true
|
|
self.mediaLibraryViewController.batchSelectModeDidChange()
|
|
}
|
|
|
|
default:
|
|
break
|
|
}
|
|
|
|
self.updateButtons(topViewController: viewController)
|
|
}
|
|
|
|
// In case back navigation was canceled, we re-apply whatever is showing.
|
|
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
|
|
self.updateButtons(topViewController: viewController)
|
|
}
|
|
}
|
|
|
|
extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate {
|
|
func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) {
|
|
attachmentDraftCollection.append(.camera(attachment: attachment))
|
|
if isInBatchSelectMode {
|
|
updateButtons(topViewController: photoCaptureViewController)
|
|
}
|
|
else {
|
|
pushApprovalViewController()
|
|
}
|
|
}
|
|
|
|
func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) {
|
|
didRequestExit()
|
|
}
|
|
|
|
func discardDraft() {
|
|
assert(attachmentDraftCollection.attachmentDrafts.count <= 1)
|
|
if let lastAttachmentDraft = attachmentDraftCollection.attachmentDrafts.last {
|
|
attachmentDraftCollection.remove(attachment: lastAttachmentDraft.attachment)
|
|
}
|
|
assert(attachmentDraftCollection.attachmentDrafts.count == 0)
|
|
}
|
|
}
|
|
|
|
extension SendMediaNavigationController: ImagePickerGridControllerDelegate {
|
|
|
|
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) {
|
|
showApprovalAfterProcessingAnyMediaLibrarySelections()
|
|
}
|
|
|
|
func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) {
|
|
didRequestExit()
|
|
}
|
|
|
|
func showApprovalAfterProcessingAnyMediaLibrarySelections() {
|
|
let mediaLibrarySelections: [MediaLibrarySelection] = self.mediaLibrarySelections.orderedValues
|
|
|
|
let backgroundBlock: (ModalActivityIndicatorViewController) -> Void = { [weak self] modal in
|
|
guard let strongSelf = self else { return }
|
|
|
|
Publishers
|
|
.MergeMany(mediaLibrarySelections.map { $0.publisher })
|
|
.collect()
|
|
.sink(
|
|
receiveCompletion: { result in
|
|
switch result {
|
|
case .finished: break
|
|
case .failure(let error):
|
|
Logger.error("failed to prepare attachments. error: \(error)")
|
|
modal.dismiss { [weak self] in
|
|
let modal: ConfirmationModal = ConfirmationModal(
|
|
targetView: self?.view,
|
|
info: ConfirmationModal.Info(
|
|
title: "IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS".localized(),
|
|
cancelTitle: "BUTTON_OK".localized(),
|
|
cancelStyle: .alert_text
|
|
)
|
|
)
|
|
self?.present(modal, animated: true)
|
|
}
|
|
}
|
|
},
|
|
receiveValue: { attachments in
|
|
Logger.debug("built all attachments")
|
|
modal.dismiss {
|
|
self?.attachmentDraftCollection.selectedFromPicker(attachments: attachments)
|
|
self?.pushApprovalViewController()
|
|
}
|
|
}
|
|
)
|
|
.store(in: &strongSelf.disposables)
|
|
}
|
|
|
|
ModalActivityIndicatorViewController.present(
|
|
fromViewController: self,
|
|
canCancel: false,
|
|
onAppear: backgroundBlock
|
|
)
|
|
}
|
|
|
|
func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool {
|
|
return mediaLibrarySelections.hasValue(forKey: asset)
|
|
}
|
|
|
|
func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPublisher: AnyPublisher<SignalAttachment, Error>) {
|
|
guard !mediaLibrarySelections.hasValue(forKey: asset) else { return }
|
|
|
|
let libraryMedia = MediaLibrarySelection(
|
|
asset: asset,
|
|
signalAttachmentPublisher: attachmentPublisher
|
|
)
|
|
mediaLibrarySelections.append(key: asset, value: libraryMedia)
|
|
updateButtons(topViewController: imagePicker)
|
|
}
|
|
|
|
func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) {
|
|
guard mediaLibrarySelections.hasValue(forKey: asset) else { return }
|
|
|
|
mediaLibrarySelections.remove(key: asset)
|
|
updateButtons(topViewController: imagePicker)
|
|
}
|
|
|
|
func imagePickerCanSelectAdditionalItems(_ imagePicker: ImagePickerGridController) -> Bool {
|
|
return attachmentDraftCollection.count <= SignalAttachment.maxAttachmentsAllowed
|
|
}
|
|
|
|
func imagePicker(_ imagePicker: ImagePickerGridController, failedToRetrieveAssetAt index: Int, forCount count: Int) {
|
|
let modal: ConfirmationModal = ConfirmationModal(
|
|
targetView: self.view,
|
|
info: ConfirmationModal.Info(
|
|
title: "IMAGE_PICKER_FAILED_TO_PROCESS_ATTACHMENTS".localized(),
|
|
cancelTitle: "BUTTON_OK".localized(),
|
|
cancelStyle: .alert_text
|
|
)
|
|
)
|
|
self.present(modal, animated: true)
|
|
}
|
|
}
|
|
|
|
extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate {
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
|
|
sendMediaNavDelegate?.sendMediaNav(self, didChangeMessageText: newMessageText)
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
|
|
guard let removedDraft = attachmentDraftCollection.attachmentDrafts.first(where: { $0.attachment == attachment}) else {
|
|
owsFailDebug("removedDraft was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
switch removedDraft.source {
|
|
case .picker(attachment: let pickerAttachment):
|
|
mediaLibrarySelections.remove(key: pickerAttachment.asset)
|
|
case .camera(attachment: _):
|
|
break
|
|
}
|
|
|
|
attachmentDraftCollection.remove(attachment: attachment)
|
|
}
|
|
|
|
func attachmentApproval(
|
|
_ attachmentApproval: AttachmentApprovalViewController,
|
|
didApproveAttachments attachments: [SignalAttachment],
|
|
forThreadId threadId: String,
|
|
threadVariant: SessionThread.Variant,
|
|
messageText: String?,
|
|
using dependencies: Dependencies
|
|
) {
|
|
sendMediaNavDelegate?.sendMediaNav(
|
|
self,
|
|
didApproveAttachments: attachments,
|
|
forThreadId: threadId,
|
|
threadVariant: threadVariant,
|
|
messageText: messageText,
|
|
using: dependencies
|
|
)
|
|
}
|
|
|
|
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
sendMediaNavDelegate?.sendMediaNavDidCancel(self)
|
|
}
|
|
|
|
func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
// Current design dicates we'll go "back" to the single thing before us.
|
|
assert(viewControllers.count == 2)
|
|
|
|
// regardless of which VC we're going "back" to, we're in "batch" mode at this point.
|
|
isInBatchSelectMode = true
|
|
mediaLibraryViewController.batchSelectModeDidChange()
|
|
|
|
popViewController(animated: true)
|
|
}
|
|
}
|
|
|
|
private enum AttachmentDraft {
|
|
case camera(attachment: SignalAttachment)
|
|
case picker(attachment: MediaLibraryAttachment)
|
|
}
|
|
|
|
private extension AttachmentDraft {
|
|
var attachment: SignalAttachment {
|
|
switch self {
|
|
case .camera(let cameraAttachment):
|
|
return cameraAttachment
|
|
case .picker(let pickerAttachment):
|
|
return pickerAttachment.signalAttachment
|
|
}
|
|
}
|
|
|
|
var source: AttachmentDraft {
|
|
return self
|
|
}
|
|
}
|
|
|
|
private final class AttachmentDraftCollection {
|
|
lazy var attachmentDrafts = [AttachmentDraft]() // Lazy to avoid https://bugs.swift.org/browse/SR-6657
|
|
|
|
static var empty: AttachmentDraftCollection {
|
|
return AttachmentDraftCollection(attachmentDrafts: [])
|
|
}
|
|
|
|
init(attachmentDrafts: [AttachmentDraft]) {
|
|
self.attachmentDrafts = attachmentDrafts
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
var count: Int {
|
|
return attachmentDrafts.count
|
|
}
|
|
|
|
var pickerAttachments: [MediaLibraryAttachment] {
|
|
return attachmentDrafts.compactMap { attachmentDraft in
|
|
switch attachmentDraft.source {
|
|
case .picker(let pickerAttachment):
|
|
return pickerAttachment
|
|
case .camera:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
var cameraAttachments: [SignalAttachment] {
|
|
return attachmentDrafts.compactMap { attachmentDraft in
|
|
switch attachmentDraft.source {
|
|
case .picker:
|
|
return nil
|
|
case .camera(let cameraAttachment):
|
|
return cameraAttachment
|
|
}
|
|
}
|
|
}
|
|
|
|
func append(_ element: AttachmentDraft) {
|
|
attachmentDrafts.append(element)
|
|
}
|
|
|
|
func remove(attachment: SignalAttachment) {
|
|
attachmentDrafts.removeAll { $0.attachment == attachment }
|
|
}
|
|
|
|
func selectedFromPicker(attachments: [MediaLibraryAttachment]) {
|
|
let pickedAttachments: Set<MediaLibraryAttachment> = Set(attachments)
|
|
let oldPickerAttachments: Set<MediaLibraryAttachment> = Set(self.pickerAttachments)
|
|
|
|
for removedAttachment in oldPickerAttachments.subtracting(pickedAttachments) {
|
|
remove(attachment: removedAttachment.signalAttachment)
|
|
}
|
|
|
|
// enumerate over new attachments to maintain order from picker
|
|
for attachment in attachments {
|
|
guard !oldPickerAttachments.contains(attachment) else {
|
|
continue
|
|
}
|
|
append(.picker(attachment: attachment))
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MediaLibrarySelection: Hashable, Equatable {
|
|
let asset: PHAsset
|
|
let signalAttachmentPublisher: AnyPublisher<SignalAttachment, Error>
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
asset.hash(into: &hasher)
|
|
}
|
|
|
|
var publisher: AnyPublisher<MediaLibraryAttachment, Error> {
|
|
let asset = self.asset
|
|
return signalAttachmentPublisher
|
|
.map { MediaLibraryAttachment(asset: asset, signalAttachment: $0) }
|
|
.eraseToAnyPublisher()
|
|
}
|
|
|
|
static func ==(lhs: MediaLibrarySelection, rhs: MediaLibrarySelection) -> Bool {
|
|
return lhs.asset == rhs.asset
|
|
}
|
|
}
|
|
|
|
private struct MediaLibraryAttachment: Hashable, Equatable {
|
|
let asset: PHAsset
|
|
let signalAttachment: SignalAttachment
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
asset.hash(into: &hasher)
|
|
}
|
|
|
|
public static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool {
|
|
return lhs.asset == rhs.asset
|
|
}
|
|
}
|
|
|
|
extension SendMediaNavigationController: DoneButtonDelegate {
|
|
var doneButtonCount: Int {
|
|
return attachmentDraftCollection.count - attachmentDraftCollection.pickerAttachments.count + mediaLibrarySelections.count
|
|
}
|
|
|
|
fileprivate func doneButtonWasTapped(_ doneButton: DoneButton) {
|
|
assert(attachmentDraftCollection.count > 0 || mediaLibrarySelections.count > 0)
|
|
showApprovalAfterProcessingAnyMediaLibrarySelections()
|
|
}
|
|
}
|
|
|
|
private protocol DoneButtonDelegate: AnyObject {
|
|
func doneButtonWasTapped(_ doneButton: DoneButton)
|
|
var doneButtonCount: Int { get }
|
|
}
|
|
|
|
private class DoneButton: UIView {
|
|
weak var delegate: DoneButtonDelegate?
|
|
let numberFormatter: NumberFormatter = NumberFormatter()
|
|
|
|
private var didTouchDownInside: Bool = false
|
|
|
|
// MARK: - UI
|
|
|
|
private let container: UIView = {
|
|
let result: UIView = UIView()
|
|
result.themeBackgroundColor = .inputButton_background
|
|
result.layer.cornerRadius = 20
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var badge: CircleView = {
|
|
let result: CircleView = CircleView()
|
|
result.themeBackgroundColor = .primary
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var badgeLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = UIFont.monospacedDigitSystemFont(
|
|
ofSize: UIFont.preferredFont(forTextStyle: .subheadline).pointSize,
|
|
weight: .regular
|
|
)
|
|
result.themeTextColor = .black // Will render on the primary color so should always be black
|
|
result.textAlignment = .center
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var chevron: UIView = {
|
|
let image: UIImage = {
|
|
guard CurrentAppContext().isRTL else { return #imageLiteral(resourceName: "small_chevron_right") }
|
|
|
|
return #imageLiteral(resourceName: "small_chevron_left")
|
|
}()
|
|
let result: UIImageView = UIImageView(image: image.withRenderingMode(.alwaysTemplate))
|
|
result.contentMode = .scaleAspectFit
|
|
result.themeTintColor = .textPrimary
|
|
result.set(.width, to: 10)
|
|
result.set(.height, to: 18)
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
init() {
|
|
super.init(frame: .zero)
|
|
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:)))
|
|
addGestureRecognizer(tapGesture)
|
|
|
|
addSubview(container)
|
|
container.pin(to: self)
|
|
|
|
badge.addSubview(badgeLabel)
|
|
badgeLabel.pin(to: badge, withInset: 4)
|
|
|
|
// Constrain to be a pill that is at least a circle, and maybe wider.
|
|
badgeLabel.autoPin(toAspectRatio: 1.0, relation: .greaterThanOrEqual)
|
|
NSLayoutConstraint.autoSetPriority(.defaultLow) {
|
|
badgeLabel.autoPinToSquareAspectRatio()
|
|
}
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [badge, chevron])
|
|
stackView.axis = .horizontal
|
|
stackView.alignment = .center
|
|
stackView.spacing = 9
|
|
|
|
addSubview(stackView)
|
|
stackView.pin(.top, to: .top, of: self, withInset: 7)
|
|
stackView.pin(.leading, to: .leading, of: self, withInset: 8)
|
|
stackView.pin(.trailing, to: .trailing, of: self, withInset: -8)
|
|
stackView.pin(.bottom, to: .bottom, of: self, withInset: -7)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
func updateCount() {
|
|
guard let delegate = delegate else { return }
|
|
|
|
badgeLabel.text = numberFormatter.string(for: delegate.doneButtonCount)
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
private func animate(
|
|
to scale: CGFloat,
|
|
themeBackgroundColor: ThemeValue,
|
|
themeBadgeBackgroundColor: ThemeValue,
|
|
themeTintColor: ThemeValue
|
|
) {
|
|
UIView.animate(withDuration: 0.25) {
|
|
self.container.transform = CGAffineTransform.identity.scale(scale)
|
|
self.badgeLabel.themeTextColor = themeTintColor
|
|
self.badge.themeBackgroundColor = themeBadgeBackgroundColor
|
|
self.container.themeBackgroundColor = themeBackgroundColor
|
|
}
|
|
}
|
|
|
|
private func expand() {
|
|
animate(
|
|
to: (InputViewButton.expandedSize / InputViewButton.size),
|
|
themeBackgroundColor: .primary,
|
|
themeBadgeBackgroundColor: .inputButton_background,
|
|
themeTintColor: .textPrimary
|
|
)
|
|
}
|
|
|
|
private func collapse() {
|
|
animate(
|
|
to: 1,
|
|
themeBackgroundColor: .inputButton_background,
|
|
themeBadgeBackgroundColor: .primary,
|
|
themeTintColor: .black
|
|
)
|
|
}
|
|
|
|
@objc func didTap(tapGesture: UITapGestureRecognizer) {
|
|
delegate?.doneButtonWasTapped(self)
|
|
}
|
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
guard
|
|
isUserInteractionEnabled,
|
|
let location: CGPoint = touches.first?.location(in: self),
|
|
bounds.contains(location)
|
|
else { return }
|
|
|
|
didTouchDownInside = true
|
|
expand()
|
|
}
|
|
|
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
guard
|
|
isUserInteractionEnabled,
|
|
let location: CGPoint = touches.first?.location(in: self),
|
|
bounds.contains(location),
|
|
didTouchDownInside
|
|
else {
|
|
if didTouchDownInside {
|
|
collapse()
|
|
}
|
|
return
|
|
}
|
|
|
|
expand()
|
|
}
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
if didTouchDownInside {
|
|
collapse()
|
|
}
|
|
|
|
didTouchDownInside = false
|
|
}
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
if didTouchDownInside {
|
|
collapse()
|
|
}
|
|
|
|
didTouchDownInside = false
|
|
}
|
|
}
|
|
|
|
// MARK: - SendMediaNavDelegate
|
|
|
|
protocol SendMediaNavDelegate: AnyObject {
|
|
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?)
|
|
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, threadVariant: SessionThread.Variant, messageText: String?, using dependencies: Dependencies)
|
|
|
|
func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
|
|
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)
|
|
}
|