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 3 years ago
parent 7715c5ea09
commit 851cc56c65

@ -17,7 +17,11 @@ final class ContextMenuVC: UIViewController {
// MARK: - UI // 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 = { private lazy var emojiBar: UIView = {
let result: UIView = UIView() let result: UIView = UIView()
@ -26,6 +30,7 @@ final class ContextMenuVC: UIViewController {
result.layer.shadowOpacity = 0.4 result.layer.shadowOpacity = 0.4
result.layer.shadowRadius = 4 result.layer.shadowRadius = 4
result.set(.height, to: ContextMenuVC.actionViewHeight) result.set(.height, to: ContextMenuVC.actionViewHeight)
result.alpha = 0
return result return result
}() }()
@ -49,6 +54,7 @@ final class ContextMenuVC: UIViewController {
result.layer.shadowOffset = CGSize.zero result.layer.shadowOffset = CGSize.zero
result.layer.shadowOpacity = 0.4 result.layer.shadowOpacity = 0.4
result.layer.shadowRadius = 4 result.layer.shadowRadius = 4
result.alpha = 0
return result return result
}() }()
@ -58,6 +64,17 @@ final class ContextMenuVC: UIViewController {
result.font = .systemFont(ofSize: Values.verySmallFontSize) result.font = .systemFont(ofSize: Values.verySmallFontSize)
result.text = cellViewModel.dateForUI.formattedForDisplay result.text = cellViewModel.dateForUI.formattedForDisplay
result.themeTextColor = .textPrimary 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 return result
}() }()
@ -156,21 +173,44 @@ final class ContextMenuVC: UIViewController {
// Timestamp // Timestamp
view.addSubview(timestampLabel) view.addSubview(timestampLabel)
timestampLabel.pin(.top, to: .top, of: menuView) timestampLabel.center(.vertical, in: snapshot)
timestampLabel.set(.height, to: ContextMenuVC.actionViewHeight)
if cellViewModel.variant == .standardOutgoing {
timestampLabel.pin(.right, to: .left, of: snapshot, withInset: -Values.smallSpacing)
}
else {
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 { if cellViewModel.variant == .standardOutgoing {
timestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing) fallbackTimestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing)
} }
else { else {
timestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing) fallbackTimestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing)
} }
// Constrains // Constrains
let timestampSize: CGSize = timestampLabel.sizeThatFits(UIScreen.main.bounds.size)
let menuHeight: CGFloat = CGFloat(menuStackView.arrangedSubviews.count) * ContextMenuVC.actionViewHeight let menuHeight: CGFloat = CGFloat(menuStackView.arrangedSubviews.count) * ContextMenuVC.actionViewHeight
let spacing: CGFloat = Values.smallSpacing let spacing: CGFloat = Values.smallSpacing
self.targetFrame = calculateFrame(menuHeight: menuHeight, spacing: spacing) 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 // Position the snapshot view in it's original message position
snapshot.frame = self.frame snapshot.frame = self.frame
emojiBar.pin(.bottom, to: .top, of: view, withInset: targetFrame.minY - spacing) emojiBar.pin(.bottom, to: .top, of: view, withInset: targetFrame.minY - spacing)
@ -202,8 +242,19 @@ final class ContextMenuVC: UIViewController {
let targetFrame: CGRect = self.targetFrame let targetFrame: CGRect = self.targetFrame
UIView.animate(withDuration: 0.3) { [weak self] in 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?.menuView.alpha = 1
self?.timestampLabel.alpha = 1
self?.fallbackTimestampLabel.alpha = 1
} }
UIView.animate( UIView.animate(
@ -222,6 +273,14 @@ final class ContextMenuVC: UIViewController {
}, },
completion: nil 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 { func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect {
@ -310,6 +369,7 @@ final class ContextMenuVC: UIViewController {
self?.menuView.alpha = 0 self?.menuView.alpha = 0
self?.emojiBar.alpha = 0 self?.emojiBar.alpha = 0
self?.timestampLabel.alpha = 0 self?.timestampLabel.alpha = 0
self?.fallbackTimestampLabel.alpha = 0
}, },
completion: { [weak self] _ in completion: { [weak self] _ in
self?.dismiss() self?.dismiss()

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

@ -1,19 +1,42 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
class EmojiPickerSheet: BaseVC { class EmojiPickerSheet: BaseVC {
let completionHandler: (EmojiWithSkinTones?) -> Void let completionHandler: (EmojiWithSkinTones?) -> Void
let dismissHandler: () -> Void let dismissHandler: () -> Void
// MARK: Components // MARK: Components
private lazy var bottomConstraint: NSLayoutConstraint = contentView.pin(.bottom, to: .bottom, of: view)
private lazy var contentView: UIView = { private lazy var contentView: UIView = {
let result = 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() let line = UIView()
line.set(.height, to: 0.5) line.themeBackgroundColor = .borderSeparator
line.backgroundColor = Colors.border.withAlphaComponent(0.5)
result.addSubview(line) result.addSubview(line)
line.set(.height, to: Values.separatorThickness)
line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result) line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result)
result.backgroundColor = Colors.modalBackground
return result return result
}() }()
@ -21,18 +44,22 @@ class EmojiPickerSheet: BaseVC {
private lazy var searchBar: SearchBar = { private lazy var searchBar: SearchBar = {
let result = SearchBar() let result = SearchBar()
result.tintColor = Colors.text result.themeTintColor = .textPrimary
result.backgroundColor = .clear result.themeBackgroundColor = .clear
result.delegate = self result.delegate = self
return result return result
}() }()
// MARK: Lifecycle // MARK: - Initialization
init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void) { init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void) {
self.completionHandler = completionHandler self.completionHandler = completionHandler
self.dismissHandler = dismissHandler self.dismissHandler = dismissHandler
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overFullScreen
} }
public required init() { public required init() {
@ -43,35 +70,55 @@ class EmojiPickerSheet: BaseVC {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Lifecycle
override public func viewDidLoad() { override public func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.themeBackgroundColor = .clear
setUpViewHierarchy() 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() { private func setUpViewHierarchy() {
view.addSubview(contentView) 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) contentView.set(.height, to: 440)
populateContentView() bottomConstraint.isActive = true
}
private func populateContentView() {
let topStackView = UIStackView() let topStackView = UIStackView()
topStackView.axis = .horizontal topStackView.axis = .horizontal
topStackView.isLayoutMarginsRelativeArrangement = true topStackView.isLayoutMarginsRelativeArrangement = true
topStackView.spacing = 8 topStackView.spacing = 8
topStackView.addArrangedSubview(searchBar)
contentView.addSubview(topStackView) contentView.addSubview(topStackView)
topStackView.set(.width, to: .width, of: contentView)
topStackView.pin(.top, to: .top, of: contentView)
topStackView.autoPinWidthToSuperview() topStackView.addArrangedSubview(searchBar)
topStackView.autoPinEdge(toSuperviewEdge: .top)
contentView.addSubview(collectionView) contentView.addSubview(collectionView)
collectionView.autoPinEdge(.top, to: .bottom, of: searchBar) collectionView.pin(.top, to: .bottom, of: searchBar)
collectionView.autoPinEdge(.bottom, to: .bottom, of: contentView) collectionView.pin(.bottom, to: .bottom, of: contentView)
collectionView.autoPinWidthToSuperview() collectionView.set(.width, to: .width, of: contentView)
collectionView.pickerDelegate = self collectionView.pickerDelegate = self
collectionView.alwaysBounceVertical = true collectionView.alwaysBounceVertical = true
} }
@ -92,16 +139,73 @@ class EmojiPickerSheet: BaseVC {
contentView.layoutIfNeeded() 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 // MARK: Interaction
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first! guard
let location = touch.location(in: view) let touch: UITouch = touches.first,
if contentView.frame.contains(location) { contentView.frame.contains(touch.location(in: view))
super.touchesBegan(touches, with: event) else {
} else {
close() close()
return
} }
super.touchesBegan(touches, with: event)
} }
@objc func close() { @objc func close() {

Loading…
Cancel
Save