element-ios/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift

244 lines
9.4 KiB
Swift

/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
@objc protocol RoomContextualMenuViewControllerDelegate: class {
func roomContextualMenuViewControllerDidTapBackgroundOverlay(_ viewController: RoomContextualMenuViewController)
}
@objcMembers
final class RoomContextualMenuViewController: UIViewController, Themable {
// MARK: - Constants
private enum Constants {
static let reactionsMenuViewVerticalMargin: CGFloat = 10.0
static let reactionsMenuViewHiddenScale: CGFloat = 0.97
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var backgroundOverlayView: UIView!
@IBOutlet private weak var menuToolbarView: RoomContextualMenuToolbarView!
@IBOutlet private weak var menuToolbarViewHeightConstraint: NSLayoutConstraint!
@IBOutlet private weak var menuToolbarViewBottomConstraint: NSLayoutConstraint!
@IBOutlet private weak var reactionsMenuContainerView: UIView!
@IBOutlet private weak var reactionsMenuViewHeightConstraint: NSLayoutConstraint!
@IBOutlet private weak var reactionsMenuViewBottomConstraint: NSLayoutConstraint!
// MARK: Private
private var theme: Theme!
private var contextualMenuItems: [RoomContextualMenuItem] = []
private var reactionsMenuViewModel: ReactionsMenuViewModel?
private weak var reactionsMenuView: ReactionsMenuView?
private var reactionsMenuViewBottomStartConstraintConstant: CGFloat?
private var reactionsMenuViewBottomEndConstraintConstant: CGFloat?
private var hiddenToolbarViewBottomConstant: CGFloat {
let bottomSafeAreaHeight: CGFloat
if #available(iOS 11.0, *) {
bottomSafeAreaHeight = self.view.safeAreaInsets.bottom
} else {
bottomSafeAreaHeight = self.bottomLayoutGuide.length
}
return -(self.menuToolbarViewHeightConstraint.constant + bottomSafeAreaHeight)
}
// MARK: Public
var contentToReactFrame: CGRect?
var shouldPerformTappedReactionAnimation: Bool {
return self.reactionsMenuView?.reactionHasBeenTapped ?? false
}
weak var delegate: RoomContextualMenuViewControllerDelegate?
// MARK: - Setup
class func instantiate(with contextualMenuItems: [RoomContextualMenuItem],
reactionsMenuViewModel: ReactionsMenuViewModel?) -> RoomContextualMenuViewController {
let viewController = StoryboardScene.RoomContextualMenuViewController.initialScene.instantiate()
viewController.theme = ThemeService.shared().theme
viewController.contextualMenuItems = contextualMenuItems
viewController.reactionsMenuViewModel = reactionsMenuViewModel
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.reactionsMenuContainerView.isHidden = true
if let reactionsMenuViewModel = self.reactionsMenuViewModel {
self.setupReactionsMenu(with: reactionsMenuViewModel)
}
self.backgroundOverlayView.isUserInteractionEnabled = true
self.menuToolbarView.fill(contextualMenuItems: self.contextualMenuItems)
self.setupBackgroundOverlayGestureRecognizers()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
}
// MARK: - Public
func showMenuToolbar() {
self.menuToolbarViewBottomConstraint.constant = 0
self.menuToolbarView.alpha = 1
}
func hideMenuToolbar() {
self.menuToolbarViewBottomConstraint.constant = self.hiddenToolbarViewBottomConstant
self.menuToolbarView.alpha = 0
}
func prepareReactionsMenuAnimations() {
guard let frame = self.contentToReactFrame else {
return
}
let menuHeight = self.reactionsMenuViewHeightConstraint.constant
let verticalMargin = Constants.reactionsMenuViewVerticalMargin
let reactionsMenuViewBottomStartConstraintConstant: CGFloat?
let reactionsMenuViewBottomEndConstraintConstant: CGFloat?
// Try to display the menu at the top of the message first
// Then, try at the bottom
// Else, keep the position defined in the storyboard
if frame.origin.y - verticalMargin >= menuHeight {
let menuViewBottomY = frame.origin.y - verticalMargin
reactionsMenuViewBottomStartConstraintConstant = menuViewBottomY + menuHeight/2
reactionsMenuViewBottomEndConstraintConstant = menuViewBottomY
} else {
let frameBottomY = frame.origin.y + frame.size.height + verticalMargin
let visibleViewHeight = self.view.frame.size.height - self.menuToolbarView.frame.size.height
if frameBottomY + menuHeight < visibleViewHeight {
let menuViewBottomY = frameBottomY + menuHeight
reactionsMenuViewBottomEndConstraintConstant = menuViewBottomY
reactionsMenuViewBottomStartConstraintConstant = menuViewBottomY - menuHeight/2
} else {
reactionsMenuViewBottomEndConstraintConstant = nil
reactionsMenuViewBottomStartConstraintConstant = nil
}
}
self.reactionsMenuViewBottomStartConstraintConstant = reactionsMenuViewBottomStartConstraintConstant
self.reactionsMenuViewBottomEndConstraintConstant = reactionsMenuViewBottomEndConstraintConstant
self.reactionsMenuContainerView.isHidden = false
}
func showReactionsMenu() {
guard let reactionsMenuView = self.reactionsMenuView else {
return
}
if let reactionsMenuViewBottomEndConstraintConstant = self.reactionsMenuViewBottomEndConstraintConstant {
self.reactionsMenuViewBottomConstraint.constant = reactionsMenuViewBottomEndConstraintConstant
}
reactionsMenuView.alpha = 1
reactionsMenuContainerView.transform = CGAffineTransform.identity
}
func hideReactionsMenu() {
guard let reactionsMenuView = self.reactionsMenuView else {
return
}
if let reactionsMenuViewBottomStartConstraintConstant = self.reactionsMenuViewBottomStartConstraintConstant {
self.reactionsMenuViewBottomConstraint.constant = reactionsMenuViewBottomStartConstraintConstant
}
reactionsMenuView.alpha = 0
let transformScale = Constants.reactionsMenuViewHiddenScale
self.reactionsMenuContainerView.transform = CGAffineTransform(scaleX: transformScale, y: transformScale)
}
func selectedReactionAnimationsIntructionsPart1() {
self.reactionsMenuView?.selectionAnimationInstructionPart1()
}
func selectedReactionAnimationsIntructionsPart2() {
self.reactionsMenuView?.selectionAnimationInstructionPart2()
}
func update(theme: Theme) {
self.menuToolbarView.update(theme: theme)
}
// MARK: - Private
private func setupReactionsMenu(with viewModel: ReactionsMenuViewModel) {
let reactionsMenuView = ReactionsMenuView.loadFromNib()
self.reactionsMenuContainerView.vc_addSubViewMatchingParent(reactionsMenuView)
reactionsMenuView.viewModel = viewModel
self.reactionsMenuView = reactionsMenuView
}
private func setupBackgroundOverlayGestureRecognizers() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handle(gestureRecognizer:)))
tapGestureRecognizer.delegate = self
let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(handle(gestureRecognizer:)))
swipeGestureRecognizer.direction = [.down, .up]
swipeGestureRecognizer.delegate = self
self.backgroundOverlayView.addGestureRecognizer(tapGestureRecognizer)
self.backgroundOverlayView.addGestureRecognizer(swipeGestureRecognizer)
}
@objc private func handle(gestureRecognizer: UIGestureRecognizer) {
self.delegate?.roomContextualMenuViewControllerDidTapBackgroundOverlay(self)
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
}
// MARK: - UIGestureRecognizerDelegate
extension RoomContextualMenuViewController: UIGestureRecognizerDelegate {
// Avoid triggering background overlay gesture recognizers when touching reactions menu
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return touch.vc_isInside(view: self.reactionsMenuContainerView) == false
}
}