session-ios/Session/Conversations/MenuActionsViewController.s...

468 lines
16 KiB
Swift
Raw Normal View History

//
2019-01-08 17:47:40 +01:00
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class MenuAction: NSObject {
let block: (MenuAction) -> Void
let image: UIImage
let title: String
let subtitle: String?
2018-07-12 06:11:36 +02:00
public init(image: UIImage, title: String, subtitle: String?, block: @escaping (MenuAction) -> Void) {
self.image = image
self.title = title
self.subtitle = subtitle
self.block = block
2018-07-12 06:11:36 +02:00
}
2018-07-12 05:55:04 +02:00
}
@objc
protocol MenuActionsViewControllerDelegate: class {
2019-03-18 16:24:09 +01:00
func menuActionsWillPresent(_ menuActionsViewController: MenuActionsViewController)
func menuActionsIsPresenting(_ menuActionsViewController: MenuActionsViewController)
func menuActionsDidPresent(_ menuActionsViewController: MenuActionsViewController)
func menuActionsIsDismissing(_ menuActionsViewController: MenuActionsViewController)
func menuActionsDidDismiss(_ menuActionsViewController: MenuActionsViewController)
}
@objc
class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
@objc
weak var delegate: MenuActionsViewControllerDelegate?
2019-03-18 16:24:09 +01:00
@objc
2019-03-19 16:13:06 +01:00
public let focusedInteraction: TSInteraction
2019-03-18 16:24:09 +01:00
2018-07-11 03:04:11 +02:00
private let focusedView: UIView
private let actionSheetView: MenuActionSheetView
2018-07-11 03:04:11 +02:00
deinit {
2018-08-23 16:37:34 +02:00
Logger.verbose("")
}
2018-07-11 00:43:20 +02:00
@objc
2019-03-19 16:13:06 +01:00
required init(focusedInteraction: TSInteraction, focusedView: UIView, actions: [MenuAction]) {
2018-07-11 00:43:20 +02:00
self.focusedView = focusedView
2019-03-18 16:24:09 +01:00
self.focusedInteraction = focusedInteraction
2018-07-11 00:43:20 +02:00
self.actionSheetView = MenuActionSheetView(actions: actions)
2018-07-11 00:43:20 +02:00
super.init(nibName: nil, bundle: nil)
actionSheetView.delegate = self
2018-07-11 00:43:20 +02:00
}
required init?(coder aDecoder: NSCoder) {
2018-08-27 16:21:03 +02:00
notImplemented()
2018-07-11 00:43:20 +02:00
}
// MARK: View LifeCycle
2018-07-11 18:53:02 +02:00
var actionSheetViewVerticalConstraint: NSLayoutConstraint?
override func loadView() {
self.view = UIView()
2018-07-11 00:43:20 +02:00
2018-07-11 03:04:11 +02:00
view.addSubview(actionSheetView)
2018-07-11 18:53:02 +02:00
actionSheetView.autoPinWidthToSuperview()
2018-07-11 03:04:11 +02:00
actionSheetView.setContentHuggingVerticalHigh()
actionSheetView.setCompressionResistanceHigh()
2018-07-11 18:53:02 +02:00
self.actionSheetViewVerticalConstraint = actionSheetView.autoPinEdge(.top, to: .bottom, of: self.view)
2018-07-11 03:04:11 +02:00
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBackground))
self.view.addGestureRecognizer(tapGesture)
2018-07-12 19:03:38 +02:00
}
2018-07-11 18:53:02 +02:00
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
2018-07-12 19:03:38 +02:00
self.animatePresentation()
}
override func viewDidDisappear(_ animated: Bool) {
2018-08-23 16:37:34 +02:00
Logger.debug("")
super.viewDidDisappear(animated)
// When the user has manually dismissed the menu, we do a nice animation
// but if the view otherwise disappears (e.g. due to resigning active),
// we still want to give the delegate the information it needs to restore it's UI.
2019-03-18 16:24:09 +01:00
delegate?.menuActionsDidDismiss(self)
}
2018-10-25 19:02:30 +02:00
// MARK: Orientation
2019-01-08 17:47:40 +01:00
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return DefaultUIInterfaceOrientationMask()
2018-10-25 19:02:30 +02:00
}
2018-07-12 19:03:38 +02:00
// MARK: Present / Dismiss animations
var snapshotView: UIView?
private func addSnapshotFocusedView() -> UIView? {
guard let snapshotView = self.focusedView.snapshotView(afterScreenUpdates: false) else {
2018-08-27 16:27:48 +02:00
owsFailDebug("snapshotView was unexpectedly nil")
2018-07-12 19:03:38 +02:00
return nil
}
view.addSubview(snapshotView)
guard let focusedViewSuperview = focusedView.superview else {
2018-08-27 16:27:48 +02:00
owsFailDebug("focusedViewSuperview was unexpectedly nil")
2018-07-12 19:03:38 +02:00
return nil
}
let convertedFrame = view.convert(focusedView.frame, from: focusedViewSuperview)
snapshotView.frame = convertedFrame
return snapshotView
}
private func animatePresentation() {
2018-07-11 18:53:02 +02:00
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
2018-08-27 16:27:48 +02:00
owsFailDebug("actionSheetViewVerticalConstraint was unexpectedly nil")
2018-07-11 18:53:02 +02:00
return
}
guard let focusedViewSuperview = focusedView.superview else {
2018-08-27 16:27:48 +02:00
owsFailDebug("focusedViewSuperview was unexpectedly nil")
return
}
2018-07-11 18:53:02 +02:00
// darken background
guard let snapshotView = addSnapshotFocusedView() else {
2018-08-27 16:27:48 +02:00
owsFailDebug("snapshotView was unexpectedly nil")
return
}
self.snapshotView = snapshotView
snapshotView.superview?.layoutIfNeeded()
2018-07-11 18:53:02 +02:00
let backgroundDuration: TimeInterval = 0.1
UIView.animate(withDuration: backgroundDuration) {
2020-08-26 04:02:27 +02:00
let alpha: CGFloat = isDarkMode ? 0.7 : 0.4
self.view.backgroundColor = UIColor.black.withAlphaComponent(alpha)
2018-07-11 18:53:02 +02:00
}
2018-07-12 19:03:38 +02:00
self.actionSheetView.superview?.layoutIfNeeded()
let oldFocusFrame = self.view.convert(focusedView.frame, from: focusedViewSuperview)
2018-07-11 18:53:02 +02:00
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(toSuperviewEdge: .bottom)
2019-03-18 16:24:09 +01:00
self.delegate?.menuActionsWillPresent(self)
2018-07-23 07:58:39 +02:00
UIView.animate(withDuration: 0.2,
2018-07-11 18:53:02 +02:00
delay: backgroundDuration,
options: .curveEaseOut,
animations: {
self.actionSheetView.superview?.layoutIfNeeded()
let newSheetFrame = self.actionSheetView.frame
var newFocusFrame = oldFocusFrame
// Position focused item just over the action sheet.
2019-03-18 16:24:09 +01:00
let overlap: CGFloat = (oldFocusFrame.maxY + self.vSpacing) - newSheetFrame.minY
newFocusFrame.origin.y = oldFocusFrame.origin.y - overlap
snapshotView.frame = newFocusFrame
2019-03-18 16:24:09 +01:00
self.delegate?.menuActionsIsPresenting(self)
},
2019-03-18 16:24:09 +01:00
completion: { (_) in
self.delegate?.menuActionsDidPresent(self)
})
}
@objc
public let vSpacing: CGFloat = 10
@objc
2019-03-19 16:13:06 +01:00
public var focusUI: UIView {
2019-03-18 16:24:09 +01:00
return actionSheetView
2018-07-11 18:53:02 +02:00
}
private func animateDismiss(action: MenuAction?) {
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
2018-08-27 16:27:48 +02:00
owsFailDebug("actionSheetVerticalConstraint was unexpectedly nil")
2019-03-18 16:24:09 +01:00
delegate?.menuActionsDidDismiss(self)
return
2018-07-11 18:53:02 +02:00
}
guard let snapshotView = self.snapshotView else {
2018-08-27 16:27:48 +02:00
owsFailDebug("snapshotView was unexpectedly nil")
2019-03-18 16:24:09 +01:00
delegate?.menuActionsDidDismiss(self)
return
}
self.actionSheetView.superview?.layoutIfNeeded()
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
let dismissDuration: TimeInterval = 0.2
2018-07-11 18:53:02 +02:00
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(.top, to: .bottom, of: self.view)
UIView.animate(withDuration: dismissDuration,
2018-07-11 18:53:02 +02:00
delay: 0,
options: .curveEaseOut,
animations: {
self.view.backgroundColor = UIColor.clear
self.actionSheetView.superview?.layoutIfNeeded()
// this helps when focused view is above navbars, etc.
snapshotView.alpha = 0
2019-03-18 16:24:09 +01:00
self.delegate?.menuActionsIsDismissing(self)
2018-07-11 18:53:02 +02:00
},
completion: { _ in
self.view.isHidden = true
2019-03-18 16:24:09 +01:00
self.delegate?.menuActionsDidDismiss(self)
if let action = action {
action.block(action)
}
2018-07-11 18:53:02 +02:00
})
}
2018-07-12 19:03:38 +02:00
// MARK: Actions
@objc
func didTapBackground() {
animateDismiss(action: nil)
}
// MARK: MenuActionSheetDelegate
func actionSheet(_ actionSheet: MenuActionSheetView, didSelectAction action: MenuAction) {
animateDismiss(action: action)
}
}
2018-07-11 03:04:11 +02:00
protocol MenuActionSheetDelegate: class {
func actionSheet(_ actionSheet: MenuActionSheetView, didSelectAction action: MenuAction)
}
2018-07-11 03:04:11 +02:00
class MenuActionSheetView: UIView, MenuActionViewDelegate {
2018-07-11 03:04:11 +02:00
private let actionStackView: UIStackView
private var actions: [MenuAction]
private var actionViews: [MenuActionView]
private var hapticFeedback: SelectionHapticFeedback
private var hasEverHighlightedAction = false
weak var delegate: MenuActionSheetDelegate?
override var bounds: CGRect {
didSet {
updateMask()
}
2018-07-11 03:04:11 +02:00
}
convenience init(actions: [MenuAction]) {
self.init(frame: CGRect.zero)
actions.forEach { self.addAction($0) }
}
override init(frame: CGRect) {
actionStackView = UIStackView()
actionStackView.axis = .vertical
actionStackView.spacing = CGHairlineWidth()
actions = []
actionViews = []
hapticFeedback = SelectionHapticFeedback()
super.init(frame: frame)
2020-08-26 04:02:27 +02:00
backgroundColor = (isDarkMode
2018-09-19 16:08:27 +02:00
? UIColor.ows_gray90
: UIColor.ows_gray05)
addSubview(actionStackView)
actionStackView.autoPinEdgesToSuperviewEdges()
self.clipsToBounds = true
let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(didTouch(gesture:)))
touchGesture.minimumPressDuration = 0.0
touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
self.addGestureRecognizer(touchGesture)
}
required init?(coder aDecoder: NSCoder) {
2018-08-27 16:21:03 +02:00
notImplemented()
}
@objc
public func didTouch(gesture: UIGestureRecognizer) {
switch gesture.state {
case .possible:
break
case .began:
let location = gesture.location(in: self)
highlightActionView(location: location, fromView: self)
case .changed:
let location = gesture.location(in: self)
highlightActionView(location: location, fromView: self)
case .ended:
2018-08-23 16:37:34 +02:00
Logger.debug("ended")
let location = gesture.location(in: self)
selectActionView(location: location, fromView: self)
case .cancelled:
2018-08-23 16:37:34 +02:00
Logger.debug("canceled")
unhighlightAllActionViews()
case .failed:
2018-08-23 16:37:34 +02:00
Logger.debug("failed")
unhighlightAllActionViews()
2021-01-21 06:43:55 +01:00
default: break
}
}
public func addAction(_ action: MenuAction) {
actions.append(action)
let actionView = MenuActionView(action: action)
actionView.delegate = self
actionViews.append(actionView)
self.actionStackView.addArrangedSubview(actionView)
}
// MARK: MenuActionViewDelegate
func actionView(_ actionView: MenuActionView, didSelectAction action: MenuAction) {
self.delegate?.actionSheet(self, didSelectAction: action)
}
// MARK:
private func updateMask() {
let cornerRadius: CGFloat = 16
let path: UIBezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
private func unhighlightAllActionViews() {
for actionView in actionViews {
actionView.isHighlighted = false
}
}
private func actionView(touchedBy touchPoint: CGPoint, fromView: UIView) -> MenuActionView? {
for actionView in actionViews {
let convertedPoint = actionView.convert(touchPoint, from: fromView)
if actionView.point(inside: convertedPoint, with: nil) {
return actionView
}
}
return nil
}
private func highlightActionView(location: CGPoint, fromView: UIView) {
guard let touchedView = actionView(touchedBy: location, fromView: fromView) else {
unhighlightAllActionViews()
return
}
if hasEverHighlightedAction, !touchedView.isHighlighted {
self.hapticFeedback.selectionChanged()
}
touchedView.isHighlighted = true
hasEverHighlightedAction = true
self.actionViews.filter { $0 != touchedView }.forEach { $0.isHighlighted = false }
}
private func selectActionView(location: CGPoint, fromView: UIView) {
guard let selectedView: MenuActionView = actionView(touchedBy: location, fromView: fromView) else {
unhighlightAllActionViews()
return
}
selectedView.isHighlighted = true
self.actionViews.filter { $0 != selectedView }.forEach { $0.isHighlighted = false }
delegate?.actionSheet(self, didSelectAction: selectedView.action)
}
}
protocol MenuActionViewDelegate: class {
func actionView(_ actionView: MenuActionView, didSelectAction action: MenuAction)
}
class MenuActionView: UIButton {
public weak var delegate: MenuActionViewDelegate?
public let action: MenuAction
2018-07-11 03:04:11 +02:00
required init(action: MenuAction) {
2018-07-11 03:04:11 +02:00
self.action = action
super.init(frame: CGRect.zero)
isUserInteractionEnabled = true
2018-08-08 21:49:22 +02:00
backgroundColor = defaultBackgroundColor
2018-07-11 18:18:45 +02:00
2020-08-26 04:02:27 +02:00
let textColor = isLightMode ? UIColor.black : UIColor.white
2018-08-16 22:22:31 +02:00
var image = action.image
2020-08-26 04:02:27 +02:00
image = image.withRenderingMode(.alwaysTemplate)
2018-08-16 22:22:31 +02:00
let imageView = UIImageView(image: image)
2021-01-29 01:46:32 +01:00
imageView.tintColor = textColor.withAlphaComponent(Values.mediumOpacity)
2018-07-11 03:04:11 +02:00
let imageWidth: CGFloat = 24
imageView.autoSetDimensions(to: CGSize(width: imageWidth, height: imageWidth))
2018-07-12 19:52:50 +02:00
imageView.isUserInteractionEnabled = false
2018-07-11 03:04:11 +02:00
let titleLabel = UILabel()
2019-12-13 05:02:05 +01:00
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
2020-08-26 04:02:27 +02:00
titleLabel.textColor = textColor
2018-07-11 03:04:11 +02:00
titleLabel.text = action.title
2018-07-12 19:52:50 +02:00
titleLabel.isUserInteractionEnabled = false
2018-07-11 03:04:11 +02:00
let subtitleLabel = UILabel()
2019-12-13 05:02:05 +01:00
subtitleLabel.font = .systemFont(ofSize: Values.smallFontSize)
2021-01-29 01:46:32 +01:00
subtitleLabel.textColor = textColor.withAlphaComponent(Values.mediumOpacity)
2018-07-11 03:04:11 +02:00
subtitleLabel.text = action.subtitle
2018-07-12 19:52:50 +02:00
subtitleLabel.isUserInteractionEnabled = false
2018-07-11 03:04:11 +02:00
let textColumn = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
textColumn.axis = .vertical
textColumn.alignment = .leading
2018-07-12 19:52:50 +02:00
textColumn.isUserInteractionEnabled = false
2018-07-11 03:04:11 +02:00
let contentRow = UIStackView(arrangedSubviews: [imageView, textColumn])
contentRow.axis = .horizontal
contentRow.alignment = .center
contentRow.spacing = 12
contentRow.isLayoutMarginsRelativeArrangement = true
2018-07-11 18:18:45 +02:00
contentRow.layoutMargins = UIEdgeInsets(top: 7, left: 16, bottom: 7, right: 16)
2018-07-12 19:52:50 +02:00
contentRow.isUserInteractionEnabled = false
2018-07-11 03:04:11 +02:00
self.addSubview(contentRow)
contentRow.autoPinEdgesToSuperviewMargins()
2018-07-11 18:31:01 +02:00
contentRow.autoSetDimension(.height, toSize: 56, relation: .greaterThanOrEqual)
self.isUserInteractionEnabled = false
2018-07-12 19:52:50 +02:00
}
2018-08-08 21:49:22 +02:00
private var defaultBackgroundColor: UIColor {
2020-08-26 04:02:27 +02:00
return isLightMode ? UIColor(hex: 0xFCFCFC) : UIColor(hex: 0x1B1B1B)
2018-08-08 21:49:22 +02:00
}
private var highlightedBackgroundColor: UIColor {
2020-08-26 04:02:27 +02:00
return isLightMode ? UIColor(hex: 0xDFDFDF) : UIColor(hex: 0x0C0C0C)
2018-08-08 21:49:22 +02:00
}
2018-07-12 19:52:50 +02:00
override var isHighlighted: Bool {
didSet {
2018-08-08 21:49:22 +02:00
self.backgroundColor = isHighlighted ? highlightedBackgroundColor : defaultBackgroundColor
2018-07-12 19:52:50 +02:00
}
}
@objc
2018-07-12 19:52:50 +02:00
func didPress(sender: Any) {
2018-08-23 16:37:34 +02:00
Logger.debug("")
self.delegate?.actionView(self, didSelectAction: action)
2018-07-11 03:04:11 +02:00
}
required init?(coder aDecoder: NSCoder) {
2018-08-27 16:21:03 +02:00
notImplemented()
2018-07-11 03:04:11 +02:00
}
}