Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed scrollbars and reduced first responder juggling #2327

Merged
merged 5 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deltachat-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
30FDB70524D1C1000066C48D /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB6F824D1C1000066C48D /* ChatViewController.swift */; };
30FDB71F24D8170E0066C48D /* TextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB71E24D8170E0066C48D /* TextMessageCell.swift */; };
30FDB72124D838240066C48D /* BaseMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB72024D838240066C48D /* BaseMessageCell.swift */; };
5F153E9A2CC38BAA00871ABE /* GiveBackMyFirstResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F153E992CC38BAA00871ABE /* GiveBackMyFirstResponder.swift */; };
5F785F6E2CB9344F003FFFB9 /* ReusableCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F785F6D2CB9344F003FFFB9 /* ReusableCellProtocol.swift */; };
7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7070FB9A2101ECBB000DC258 /* NewGroupController.swift */; };
7092474120B3869500AF8799 /* ContactDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7092474020B3869500AF8799 /* ContactDetailViewController.swift */; };
Expand Down Expand Up @@ -433,6 +434,7 @@
30FDB6F824D1C1000066C48D /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = "<group>"; };
30FDB71E24D8170E0066C48D /* TextMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = "<group>"; };
30FDB72024D838240066C48D /* BaseMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMessageCell.swift; sourceTree = "<group>"; };
5F153E992CC38BAA00871ABE /* GiveBackMyFirstResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiveBackMyFirstResponder.swift; sourceTree = "<group>"; };
5F785F6D2CB9344F003FFFB9 /* ReusableCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableCellProtocol.swift; sourceTree = "<group>"; };
7070FB9A2101ECBB000DC258 /* NewGroupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGroupController.swift; sourceTree = "<group>"; };
7092474020B3869500AF8799 /* ContactDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1025,6 +1027,7 @@
AEACE2E41FB32E1900DCDD78 /* Utils.swift */,
AE851AC4227C755A00ED86F0 /* Protocols.swift */,
3095A350237DD1F700AB07F7 /* MediaPicker.swift */,
5F153E992CC38BAA00871ABE /* GiveBackMyFirstResponder.swift */,
30AC265E237F1807002A943F /* AvatarHelper.swift */,
302B84C42396627F001C261F /* RelayHelper.swift */,
AE1988A423EB2FBA00B4CD5F /* Errors.swift */,
Expand Down Expand Up @@ -1630,6 +1633,7 @@
D8FB04002B4F0CF100A355F8 /* EmojiView.swift in Sources */,
3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */,
3080A035277DE30100E74565 /* String+Extensions.swift in Sources */,
5F153E9A2CC38BAA00871ABE /* GiveBackMyFirstResponder.swift in Sources */,
302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */,
7A9FB1441FB061E2001FEA36 /* AppDelegate.swift in Sources */,
30C7D5EE28F47E620078D24C /* MessageCounter.swift in Sources */,
Expand Down
89 changes: 44 additions & 45 deletions deltachat-ios/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import UIKit
import AVFoundation
import DcCore
import SDWebImage
import Combine

class ChatViewController: UITableViewController, UITableViewDropDelegate {
public let chatId: Int

private var dcContext: DcContext
private var messageIds: [Int] = []
private var isVisibleToUser: Bool = false
private var wasInputBarFirstResponder = false
private var reactionMessageId: Int?
private var contextMenuVisible = false

Expand Down Expand Up @@ -157,11 +157,18 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
get { customInputAccessoryView }
set { customInputAccessoryView = newValue }
}

/// Set this to false if you are doing something with the UI inside this view controller that
/// requires the inputAccessoryView to be hidden. Do not set this when navigating,
/// because UIKit should automatically return the firstResponder.
private var shouldBecomeFirstResponder: Bool = false
override var canBecomeFirstResponder: Bool {
if let presentedViewController {
if let presentedViewController, !presentedViewController.isBeingDismissed {
// Should not show inputAccessoryView when anything other than searchController is presented
return presentedViewController is UISearchController && shouldBecomeFirstResponder
} else if navigationController?.topViewController != self {
// Don't show inputAccessoryView when not top view controller
return false
} else {
return shouldBecomeFirstResponder
}
Expand Down Expand Up @@ -190,6 +197,12 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
return view
}()

private var _bag: [Any/*Cancellable*/] = []
@available(iOS 13.0, *) private var bag: [AnyCancellable] {
get { _bag.compactMap { $0 as? AnyCancellable } }
set { _bag = newValue }
}

init(dcContext: DcContext, chatId: Int, highlightedMsg: Int? = nil) {
self.dcContext = dcContext
self.chatId = chatId
Expand Down Expand Up @@ -233,6 +246,12 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
tableView.transform = CGAffineTransform(scaleX: 1, y: -1)
// Since the view is flipped, its safeArea will be flipped, luckily we can ignore it
tableView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
tableView.automaticallyAdjustsScrollIndicatorInsets = false
tableView.publisher(for: \.contentInset)
.assign(to: \.scrollIndicatorInsets, on: tableView)
.store(in: &bag)
}

navigationController?.setNavigationBarHidden(false, animated: false)

Expand All @@ -245,27 +264,27 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

// Binding to the tableView will enable interactive dismissal
keyboardManager?.bind(to: tableView)
keyboardManager?.on(event: .willShow) { [weak self] notification in
guard let self else { return }
let globalTableViewFrame = self.tableView.convert(tableView.bounds, to: tableView.window)
keyboardManager?.on(event: .willShow) { [tableView = tableView!] notification in
// Using superview instead of window here because in iOS 13+ a modal can change
// the frame of the vc it is presented over which causes this calculation to be off.
let globalTableViewFrame = tableView.convert(tableView.bounds, to: tableView.superview)
let intersection = globalTableViewFrame.intersection(notification.endFrame)
let inset = intersection.height
// willShow is sometimes called when the keyboard is being hidden or when the kb was
// already shown due to interactive dismissal getting canceled.
guard self.tableView.contentInset.top != inset else { return }
guard tableView.contentInset.top != inset else { return }
UIView.animate(withDuration: notification.timeInterval, delay: 0, options: notification.animationOptions) {
self.tableView.contentInset.top = inset
if self.tableView.contentOffset.y < 30 {
tableView.contentInset.top = inset
if tableView.contentOffset.y < 30 {
// If user is less than 30 away from the bottom, we scroll
// the bottom of the content to the top of the keyboard.
self.tableView.contentOffset.y -= inset + self.tableView.contentOffset.y
tableView.contentOffset.y -= inset + tableView.contentOffset.y
}
}
}
keyboardManager?.on(event: .willHide) { [weak self] notification in
guard let self else { return }
keyboardManager?.on(event: .willHide) { [tableView, inputAccessoryView] notification in
UIView.animate(withDuration: notification.timeInterval, delay: 0, options: notification.animationOptions) {
self.tableView.contentInset.top = self.inputAccessoryView?.frame.height ?? 0
tableView?.contentInset.top = inputAccessoryView?.frame.height ?? 0
}
}

Expand Down Expand Up @@ -341,17 +360,12 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

if dcChat.canSend {
shouldBecomeFirstResponder = true

if wasInputBarFirstResponder {
messageInputBar.inputTextView.becomeFirstResponder()
} else {
becomeFirstResponder()
}
}

messageInputBar.scrollDownButton.isHidden = true

if isMovingToParent { // being pushed
becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.tableView.contentInset.top = self.inputAccessoryView?.bounds.height ?? 0
if let msgId = self.highlightedMsg, self.messageIds.firstIndex(of: msgId) != nil {
Expand Down Expand Up @@ -384,21 +398,13 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

// the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
wasInputBarFirstResponder = messageInputBar.inputTextView.isFirstResponder
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AppStateRestorer.shared.resetLastActiveChat()
handleUserVisibility(isVisible: false)
audioController.stopAnyOngoingPlaying()
messageInputBar.inputTextView.resignFirstResponder()
if !wasInputBarFirstResponder {
resignFirstResponder()
}

wasInputBarFirstResponder = false
shouldBecomeFirstResponder = false
}

override func viewSafeAreaInsetsDidChange() {
Expand All @@ -412,7 +418,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
if parent == nil {
// going back to previous screen
draft.save(context: dcContext)
keyboardManager = nil
}
}

Expand Down Expand Up @@ -681,7 +686,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

private func configureContactRequestBar() {
messageInputBar.separatorLine.backgroundColor = DcColors.colorDisabled
shouldBecomeFirstResponder = true

let bar: ChatContactRequestBar
if dcChat.isProtectionBroken {
Expand Down Expand Up @@ -745,10 +749,13 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

if #available(iOS 13.0, *) {
action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?.sd_tintedImage(with: DcColors.defaultInverseColor)
action.image = UIImage(systemName: "arrowshape.turn.up.left.fill")?
.sd_tintedImage(with: DcColors.defaultInverseColor)?
.sd_flippedImage(withHorizontal: false, vertical: true)
action.backgroundColor = DcColors.chatBackgroundColor.withAlphaComponent(0.25)
} else {
action.image = UIImage(named: "ic_reply_black")
action.image = UIImage(named: "ic_reply_black")?
.sd_flippedImage(withHorizontal: false, vertical: true)
action.backgroundColor = .systemBlue
}
action.accessibilityElements = nil
Expand Down Expand Up @@ -1242,9 +1249,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

alert.addAction(sendContactAction)

alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in self.shouldBecomeFirstResponder = true }))

shouldBecomeFirstResponder = false
alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))

self.present(alert, animated: true, completion: {
// unfortunately, voiceMessageAction.accessibilityHint does not work,
Expand Down Expand Up @@ -1481,17 +1486,14 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func webxdcButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showWebxdcSelector()
}

private func documentActionPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showDocumentLibrary()
}

private func voiceMessageButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showVoiceMessageRecorder()
}

Expand All @@ -1500,13 +1502,10 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func galleryButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
showPhotoVideoLibrary(delegate: self)
}

private func showContactList(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true

let contactList = SendContactViewController(dcContext: dcContext)
contactList.delegate = self

Expand All @@ -1522,7 +1521,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func locationStreamingButtonPressed(_ action: UIAlertAction) {
shouldBecomeFirstResponder = true
let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
if isLocationStreaming {
locationStreamingFor(seconds: 0)
Expand Down Expand Up @@ -1775,9 +1773,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func selectMore(at indexPath: IndexPath) {
messageInputBar.inputTextView.resignFirstResponder()
resignFirstResponder()

setEditing(isEditing: true, selectedAtIndexPath: indexPath)
if UIAccessibility.isVoiceOverRunning {
forceVoiceOverFocussingCell(at: indexPath, postingFinished: nil)
Expand Down Expand Up @@ -1901,6 +1896,9 @@ extension ChatViewController {
cell.reactionsView.isHidden = true
contextMenuVisible = true

shouldBecomeFirstResponder = false
messageInputBar.inputTextView.resignFirstResponder()

updateScrollDownButtonVisibility()
}

Expand All @@ -1920,6 +1918,9 @@ extension ChatViewController {
cell.reactionsView.isHidden = false
contextMenuVisible = false

shouldBecomeFirstResponder = true
becomeFirstResponder()

updateScrollDownButtonVisibility()
}

Expand Down Expand Up @@ -2235,8 +2236,6 @@ extension ChatViewController {
_ = handleSelection(indexPath: indexPath)
}
self.updateTitle()
shouldBecomeFirstResponder = isEditing
becomeFirstResponder()
if refreshMessagesAfterEditing && isEditing == false {
refreshMessages()
}
Expand Down
2 changes: 1 addition & 1 deletion deltachat-ios/Chat/Views/DraftArea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import UIKit
import DcCore

public class DraftArea: UIView, InputItem {
public var inputBarAccessoryView: InputBarAccessoryView?
public weak var inputBarAccessoryView: InputBarAccessoryView?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! excellent!

public var parentStackViewPosition: InputStackView.Position?
public func textViewDidChangeAction(with textView: InputTextView) {}
public func keyboardSwipeGestureAction(with gesture: UISwipeGestureRecognizer) {}
Expand Down
Loading