UI: hide keyboard when presenting context menu for message
Gitlab: #239
Change-Id: I73bbce31461a6f3e595a93bdb1e2cce7e536d8d9
diff --git a/Ring/Ring/Extensions/View+Helpers.swift b/Ring/Ring/Extensions/View+Helpers.swift
index 641672c..35d5045 100644
--- a/Ring/Ring/Extensions/View+Helpers.swift
+++ b/Ring/Ring/Extensions/View+Helpers.swift
@@ -19,6 +19,7 @@
*/
import SwiftUI
+import Combine
extension View {
func placeholder<Content: View>(
@@ -97,3 +98,22 @@
}
}
}
+
+extension Publishers {
+ static var keyboardHeight: AnyPublisher<CGFloat, Never> {
+ let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
+ .map { $0.keyboardHeight }
+
+ let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
+ .map { _ in CGFloat(0) }
+
+ return MergeMany(willShow, willHide)
+ .eraseToAnyPublisher()
+ }
+}
+
+extension Notification {
+ var keyboardHeight: CGFloat {
+ (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0
+ }
+}
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift
index c470c9e..e59a6b5 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift
@@ -20,6 +20,15 @@
import SwiftUI
+enum ContextMenuPresentingState {
+ case none
+ case shouldPresent
+ case willDismissWithoutAction
+ case willDismissWithTextEditingAction
+ case willDismissWithAction
+ case dismissed
+}
+
struct VisualEffect: UIViewRepresentable {
@SwiftUI.State var style: UIBlurEffect.Style
var withVibrancy: Bool
@@ -35,7 +44,7 @@
struct ContextMenuView: View {
var model: ContextMenuVM
- @Binding var showContextMenu: Bool
+ @Binding var presentingState: ContextMenuPresentingState
// animations
@SwiftUI.State private var blurAmount = 0.0
@SwiftUI.State private var backgroundScale: CGFloat = 1.00
@@ -87,7 +96,8 @@
.background(makeBackground())
}
.onTapGesture {
- withAnimation(Animation.easeOut(duration: 0.2)) {
+ presentingState = .willDismissWithoutAction
+ withAnimation(Animation.easeOut(duration: 0.3)) {
scrollViewHeight = model.messageFrame.height
blurAmount = 0
backgroundScale = 1.00
@@ -99,8 +109,8 @@
messageOffsetDiff = 0
cornerRadius = 0
}
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
- showContextMenu = false
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ presentingState = .dismissed
}
}
.onAppear(perform: {
@@ -146,8 +156,13 @@
ForEach(model.menuItems) { item in
VStack(spacing: 0) {
Button {
- showContextMenu = false
+ let shouldShowKeyboard = item == .copy || item == .deleteMessage
+ let state: ContextMenuPresentingState = shouldShowKeyboard ? .willDismissWithAction : .willDismissWithTextEditingAction
+ presentingState = state
model.presentingMessage.model.contextMenuSelect(item: item)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ presentingState = .dismissed
+ }
} label: {
HStack {
Spacer()
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift
index 5191374..625b9e4 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagePanelView.swift
@@ -123,8 +123,8 @@
struct MessagePanelView: View {
@StateObject var model: MessagePanelVM
+ @Binding var isFocused: Bool
@SwiftUI.State private var text: String = ""
- @SwiftUI.State private var isFocused: Bool = false
@SwiftUI.State private var textHeight: CGFloat = 0
let padding: CGFloat = 10
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
index 5d4b104..bb2f3ba 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
@@ -22,6 +22,7 @@
import SwiftUI
import UIKit
import RxSwift
+import Combine
struct Flipped: ViewModifier {
func body(content: Content) -> some View {
@@ -52,15 +53,20 @@
let scrollReserved = UIScreen.main.bounds.height * 1.5
// context menu
- @SwiftUI.State private var showContextMenu = false
+ @SwiftUI.State var contextMenuPresentingState: ContextMenuPresentingState = .none
@SwiftUI.State private var currentSnapshot: UIImage?
@SwiftUI.State private var presentingMessage: MessageBubbleView?
@SwiftUI.State private var messageFrame: CGRect?
var contextMenuModel = ContextMenuVM()
@SwiftUI.State private var screenHeight: CGFloat = 0
- @SwiftUI.State private var showReactionsView = false
- @SwiftUI.State private var reactionsForMessage: ReactionsContainerModel?
@SwiftUI.State private var messageContainerHeight: CGFloat = 0
+ @SwiftUI.State private var shouldHideActiveKeyboard = false
+ @SwiftUI.State var isMessageBarFocused: Bool = false
+ @SwiftUI.State var keyboardHeight: CGFloat = 0
+
+ // reactions
+ @SwiftUI.State private var reactionsForMessage: ReactionsContainerModel?
+ @SwiftUI.State private var showReactionsView = false
var body: some View {
ZStack {
@@ -74,8 +80,8 @@
}
}
.layoutPriority(1)
- .padding(.bottom, messageContainerHeight - 30)
- MessagePanelView(model: model.messagePanel)
+ .padding(.bottom, shouldHideActiveKeyboard ? keyboardHeight : messageContainerHeight - 30)
+ MessagePanelView(model: model.messagePanel, isFocused: $isMessageBarFocused)
.alignmentGuide(VerticalAlignment.center) { dimensions in
DispatchQueue.main.async {
self.messageContainerHeight = dimensions.height
@@ -83,21 +89,30 @@
return dimensions[VerticalAlignment.center]
}
}
- .overlay(showContextMenu && contextMenuModel.presentingMessage != nil ? makeOverlay() : nil)
+ .overlay(contextMenuPresentingState == .shouldPresent && contextMenuModel.presentingMessage != nil ? makeOverlay() : nil)
// hide navigation bar when presenting context menu
- .onChange(of: showContextMenu) { newValue in
- model.hideNavigationBar.accept(newValue)
+ .onChange(of: contextMenuPresentingState) { newValue in
+ let shouldHide = newValue == .shouldPresent
+ model.hideNavigationBar.accept(shouldHide)
}
// hide context menu overly when device is rotated
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
if screenHeight != UIScreen.main.bounds.size.height && screenHeight != 0 {
screenHeight = UIScreen.main.bounds.size.height
- showContextMenu = false
+ contextMenuPresentingState = .dismissed
+ self.shouldHideActiveKeyboard = false
}
}
.onAppear(perform: {
screenHeight = UIScreen.main.bounds.size.height
})
+
+ .onChange(of: contextMenuPresentingState, perform: { state in
+ contextMenuPresentingStateChanged(state)
+ })
+ .onReceive(Publishers.keyboardHeight) { height in
+ handleKeyboardHeightChange(height)
+ }
if model.shouldShowMap {
LocationSharingView(model: model)
}
@@ -132,7 +147,7 @@
}
func makeOverlay() -> some View {
- return ContextMenuView(model: contextMenuModel, showContextMenu: $showContextMenu)
+ return ContextMenuView(model: contextMenuModel, presentingState: $contextMenuPresentingState)
}
private func createMessagesStackView() -> some View {
@@ -194,13 +209,21 @@
private func createMessageRowView(for message: MessageContainerModel) -> some View {
MessageRowView(messageModel: message, onLongPress: {(frame, message) in
- if showContextMenu == true {
+ if contextMenuPresentingState != .dismissed && contextMenuPresentingState != .none {
return
}
model.hideNavigationBar.accept(true)
contextMenuModel.presentingMessage = message
contextMenuModel.messageFrame = frame
- showContextMenu = true
+ /*
+ If the keyboard is open, it should be closed.
+ Once the context menu is removed, the keyboard
+ should be shown again.
+ */
+ if keyboardHeight > 0 {
+ hideKeyboardIfNeed()
+ }
+ contextMenuPresentingState = .shouldPresent
}, showReactionsView: {message in
reactionsForMessage = message
showReactionsView.toggle()
@@ -248,6 +271,58 @@
.ignoresSafeArea(.container, edges: [])
.shadowForConversation()
}
+
+ private func hideKeyboardIfNeed() {
+ if keyboardHeight > 0 {
+ withAnimation {
+ self.shouldHideActiveKeyboard = true
+ }
+ self.hideKeyboard()
+ self.isMessageBarFocused = false
+ }
+ }
+
+ private func handleKeyboardHeightChange(_ height: CGFloat) {
+ /*
+ If shouldHideActiveKeyboard is true, we don't track the
+ keyboard height, as the keyboard is temporarily hidden
+ and expected to reappear once the context menu is removed.
+ */
+ if !shouldHideActiveKeyboard {
+ DispatchQueue.main.async {
+ withAnimation {
+ keyboardHeight = height
+ }
+ }
+ }
+ }
+
+ private func contextMenuPresentingStateChanged(_ state: ContextMenuPresentingState) {
+ /*
+ When the context menu is removed, we need to reopen
+ the keyboard and reactivate the message bar if it was
+ active before, except in cases where a selected action
+ will trigger the keyboard itself. For example, actions
+ like editing or replying to a message.
+ */
+ switch state {
+ case .willDismissWithoutAction, .willDismissWithAction:
+ if shouldHideActiveKeyboard {
+ isMessageBarFocused = true
+ withAnimation {
+ shouldHideActiveKeyboard = false
+ }
+ }
+ case .none, .shouldPresent, .dismissed:
+ break
+ case .willDismissWithTextEditingAction:
+ if shouldHideActiveKeyboard {
+ withAnimation {
+ shouldHideActiveKeyboard = false
+ }
+ }
+ }
+ }
}
func topVC() -> UIViewController? {
diff --git a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift
index 5d09128..0bd7eb2 100644
--- a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift
+++ b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift
@@ -212,7 +212,7 @@
let password = BehaviorRelay<String>(value: "")
let confirmPassword = BehaviorRelay<String>(value: "")
let notificationSwitch = BehaviorRelay<Bool>(value: true)
- let nameRegistrationTimeout:CGFloat = 30
+ let nameRegistrationTimeout: CGFloat = 30
lazy var usernameValidationState = BehaviorRelay<UsernameValidationState>(value: .unknown)
lazy var canAskForAccountCreation: Observable<Bool> = {
return Observable.combineLatest(self.usernameValidationState.asObservable(),