Fixed a couple of bugs with the emoji picker and context menu

Cleaned up the context menu appearance and the message timestamp appearing off screen issue
Fixed an issue with keyboard avoidance on the emoji picker
pull/672/head
Morgan Pretty 2 years ago
parent 7715c5ea09
commit 851cc56c65

@ -17,7 +17,11 @@ final class ContextMenuVC: UIViewController {
// MARK: - UI
private lazy var blurView: UIVisualEffectView = UIVisualEffectView(effect: nil)
public override var preferredStatusBarStyle: UIStatusBarStyle {
return ThemeManager.currentTheme.statusBarStyle
}
private lazy var blurView: UIVisualEffectView = UIVisualEffectView()
private lazy var emojiBar: UIView = {
let result: UIView = UIView()
@ -26,6 +30,7 @@ final class ContextMenuVC: UIViewController {
result.layer.shadowOpacity = 0.4
result.layer.shadowRadius = 4
result.set(.height, to: ContextMenuVC.actionViewHeight)
result.alpha = 0
return result
}()
@ -49,6 +54,7 @@ final class ContextMenuVC: UIViewController {
result.layer.shadowOffset = CGSize.zero
result.layer.shadowOpacity = 0.4
result.layer.shadowRadius = 4
result.alpha = 0
return result
}()
@ -58,6 +64,17 @@ final class ContextMenuVC: UIViewController {
result.font = .systemFont(ofSize: Values.verySmallFontSize)
result.text = cellViewModel.dateForUI.formattedForDisplay
result.themeTextColor = .textPrimary
result.alpha = 0
return result
}()
private lazy var fallbackTimestampLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.verySmallFontSize)
result.text = cellViewModel.dateForUI.formattedForDisplay
result.themeTextColor = .textPrimary
result.alpha = 0
return result
}()
@ -156,21 +173,44 @@ final class ContextMenuVC: UIViewController {
// Timestamp
view.addSubview(timestampLabel)
timestampLabel.pin(.top, to: .top, of: menuView)
timestampLabel.set(.height, to: ContextMenuVC.actionViewHeight)
timestampLabel.center(.vertical, in: snapshot)
if cellViewModel.variant == .standardOutgoing {
timestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing)
timestampLabel.pin(.right, to: .left, of: snapshot, withInset: -Values.smallSpacing)
}
else {
timestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing)
timestampLabel.pin(.left, to: .right, of: snapshot, withInset: Values.smallSpacing)
}
view.addSubview(fallbackTimestampLabel)
fallbackTimestampLabel.pin(.top, to: .top, of: menuView)
fallbackTimestampLabel.set(.height, to: ContextMenuVC.actionViewHeight)
if cellViewModel.variant == .standardOutgoing {
fallbackTimestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing)
}
else {
fallbackTimestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing)
}
// Constrains
let timestampSize: CGSize = timestampLabel.sizeThatFits(UIScreen.main.bounds.size)
let menuHeight: CGFloat = CGFloat(menuStackView.arrangedSubviews.count) * ContextMenuVC.actionViewHeight
let spacing: CGFloat = Values.smallSpacing
self.targetFrame = calculateFrame(menuHeight: menuHeight, spacing: spacing)
// Decide which timestamp label should be used based on whether it'll go off screen
self.timestampLabel.isHidden = {
switch cellViewModel.variant {
case .standardOutgoing:
return ((self.targetFrame.minX - timestampSize.width - Values.mediumSpacing) < 0)
default:
return ((self.targetFrame.maxX + timestampSize.width + Values.mediumSpacing) > UIScreen.main.bounds.width)
}
}()
self.fallbackTimestampLabel.isHidden = !self.timestampLabel.isHidden
// Position the snapshot view in it's original message position
snapshot.frame = self.frame
emojiBar.pin(.bottom, to: .top, of: view, withInset: targetFrame.minY - spacing)
@ -202,8 +242,19 @@ final class ContextMenuVC: UIViewController {
let targetFrame: CGRect = self.targetFrame
UIView.animate(withDuration: 0.3) { [weak self] in
self?.blurView.effect = UIBlurEffect(style: .regular)
self?.blurView.effect = UIBlurEffect(
style: (ThemeManager.currentTheme.interfaceStyle == .light ?
.light :
.dark
)
)
}
UIView.animate(withDuration: 0.2) { [weak self] in
self?.emojiBar.alpha = 1
self?.menuView.alpha = 1
self?.timestampLabel.alpha = 1
self?.fallbackTimestampLabel.alpha = 1
}
UIView.animate(
@ -222,6 +273,14 @@ final class ContextMenuVC: UIViewController {
},
completion: nil
)
// Change the blur effect on theme change
ThemeManager.onThemeChange(observer: blurView) { [weak self] theme, _ in
switch theme.interfaceStyle {
case .light: self?.blurView.effect = UIBlurEffect(style: .light)
default: self?.blurView.effect = UIBlurEffect(style: .dark)
}
}
}
func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect {
@ -310,6 +369,7 @@ final class ContextMenuVC: UIViewController {
self?.menuView.alpha = 0
self?.emojiBar.alpha = 0
self?.timestampLabel.alpha = 0
self?.fallbackTimestampLabel.alpha = 0
},
completion: { [weak self] _ in
self?.dismiss()

@ -1305,7 +1305,7 @@ extension ConversationVC:
self?.showInputAccessoryView()
}
)
emojiPicker.modalPresentationStyle = .overFullScreen
present(emojiPicker, animated: true, completion: nil)
}

@ -1,19 +1,42 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
class EmojiPickerSheet: BaseVC {
let completionHandler: (EmojiWithSkinTones?) -> Void
let dismissHandler: () -> Void
// MARK: Components
private lazy var bottomConstraint: NSLayoutConstraint = contentView.pin(.bottom, to: .bottom, of: view)
private lazy var contentView: UIView = {
let result = UIView()
let backgroundView = UIView()
backgroundView.themeBackgroundColor = .backgroundSecondary
backgroundView.alpha = Values.lowOpacity
result.addSubview(backgroundView)
backgroundView.pin(to: result)
let blurView: UIVisualEffectView = UIVisualEffectView()
result.addSubview(blurView)
blurView.pin(to: result)
ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in
switch theme.interfaceStyle {
case .light: blurView?.effect = UIBlurEffect(style: .light)
default: blurView?.effect = UIBlurEffect(style: .dark)
}
}
let line = UIView()
line.set(.height, to: 0.5)
line.backgroundColor = Colors.border.withAlphaComponent(0.5)
line.themeBackgroundColor = .borderSeparator
result.addSubview(line)
line.set(.height, to: Values.separatorThickness)
line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result)
result.backgroundColor = Colors.modalBackground
return result
}()
@ -21,18 +44,22 @@ class EmojiPickerSheet: BaseVC {
private lazy var searchBar: SearchBar = {
let result = SearchBar()
result.tintColor = Colors.text
result.backgroundColor = .clear
result.themeTintColor = .textPrimary
result.themeBackgroundColor = .clear
result.delegate = self
return result
}()
// MARK: Lifecycle
// MARK: - Initialization
init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void) {
self.completionHandler = completionHandler
self.dismissHandler = dismissHandler
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overFullScreen
}
public required init() {
@ -43,35 +70,55 @@ class EmojiPickerSheet: BaseVC {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Lifecycle
override public func viewDidLoad() {
super.viewDidLoad()
view.themeBackgroundColor = .clear
setUpViewHierarchy()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleKeyboardWillChangeFrameNotification(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleKeyboardWillHideNotification(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
private func setUpViewHierarchy() {
view.addSubview(contentView)
contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view)
contentView.pin(.leading, to: .leading, of: view)
contentView.pin(.trailing, to: .trailing, of: view)
contentView.set(.height, to: 440)
populateContentView()
}
private func populateContentView() {
bottomConstraint.isActive = true
let topStackView = UIStackView()
topStackView.axis = .horizontal
topStackView.isLayoutMarginsRelativeArrangement = true
topStackView.spacing = 8
topStackView.addArrangedSubview(searchBar)
contentView.addSubview(topStackView)
topStackView.autoPinWidthToSuperview()
topStackView.autoPinEdge(toSuperviewEdge: .top)
topStackView.set(.width, to: .width, of: contentView)
topStackView.pin(.top, to: .top, of: contentView)
topStackView.addArrangedSubview(searchBar)
contentView.addSubview(collectionView)
collectionView.autoPinEdge(.top, to: .bottom, of: searchBar)
collectionView.autoPinEdge(.bottom, to: .bottom, of: contentView)
collectionView.autoPinWidthToSuperview()
collectionView.pin(.top, to: .bottom, of: searchBar)
collectionView.pin(.bottom, to: .bottom, of: contentView)
collectionView.set(.width, to: .width, of: contentView)
collectionView.pickerDelegate = self
collectionView.alwaysBounceVertical = true
}
@ -92,16 +139,73 @@ class EmojiPickerSheet: BaseVC {
contentView.layoutIfNeeded()
}
// MARK: - Keyboard Avoidance
@objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
// Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600
// and https://stackoverflow.com/a/25260930 to better understand what we are
// doing with the UIViewAnimationOptions
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0)
let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue))
let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16))
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY)
UIView.animate(
withDuration: duration,
delay: 0,
options: options,
animations: { [weak self] in
// Note: We don't need to completely avoid the keyboard here for this to be useful (and
// probably don't want to on smaller screens anyway)
self?.bottomConstraint.constant = -(keyboardTop / 2)
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
},
completion: nil
)
}
@objc func handleKeyboardWillHideNotification(_ notification: Notification) {
// Please refer to https://github.com/mapbox/mapbox-navigation-ios/issues/1600
// and https://stackoverflow.com/a/25260930 to better understand what we are
// doing with the UIViewAnimationOptions
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
let duration = ((userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval) ?? 0)
let curveValue: Int = ((userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) ?? Int(UIView.AnimationOptions.curveEaseInOut.rawValue))
let options: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: UInt(curveValue << 16))
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY)
UIView.animate(
withDuration: duration,
delay: 0,
options: options,
animations: { [weak self] in
self?.bottomConstraint.constant = 0
self?.view.setNeedsLayout()
self?.view.layoutIfNeeded()
},
completion: nil
)
}
// MARK: Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: view)
if contentView.frame.contains(location) {
super.touchesBegan(touches, with: event)
} else {
guard
let touch: UITouch = touches.first,
contentView.frame.contains(touch.location(in: view))
else {
close()
return
}
super.touchesBegan(touches, with: event)
}
@objc func close() {

Loading…
Cancel
Save