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

Draft: improve scrolling behavior #1767

Closed
wants to merge 7 commits into from
Closed
4 changes: 4 additions & 0 deletions deltachat-ios.xcodeproj/project.pbxproj
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
21D6C941260623F500D0755A /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D6C9392606190600D0755A /* NotificationManager.swift */; };
3001B560294265C200816B0C /* AtomicBoolean.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3001B55F294265C200816B0C /* AtomicBoolean.swift */; };
3008CB7224F93EB900E6A617 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */; };
3008CB7424F9436C00E6A617 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */; };
3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7524F95B6D00E6A617 /* AudioController.swift */; };
Expand Down Expand Up @@ -253,6 +254,7 @@
21D6C9392606190600D0755A /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
21EE28844E7A690D73BF5285 /* Pods-deltachat-iosTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-deltachat-iosTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-deltachat-iosTests/Pods-deltachat-iosTests.debug.xcconfig"; sourceTree = "<group>"; };
2F7009234DB9408201A6CDCB /* Pods_deltachat_iosTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_deltachat_iosTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3001B55F294265C200816B0C /* AtomicBoolean.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicBoolean.swift; sourceTree = "<group>"; };
3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = "<group>"; };
3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
3008CB7524F95B6D00E6A617 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1000,6 +1002,7 @@
30E83EFC289BF32C0035614C /* ShortcutManager.swift */,
30CE137728D9C40700158DF4 /* ChatDropInteraction.swift */,
30E6E359293FEEA70093871E /* SimpleLogger.swift */,
3001B55F294265C200816B0C /* AtomicBoolean.swift */,
);
path = Helper;
sourceTree = "<group>";
Expand Down Expand Up @@ -1533,6 +1536,7 @@
21D6C941260623F500D0755A /* NotificationManager.swift in Sources */,
3080A023277DE09900E74565 /* SeparatorLine.swift in Sources */,
302B84C72396770B001C261F /* RelayHelper.swift in Sources */,
3001B560294265C200816B0C /* AtomicBoolean.swift in Sources */,
305961CF2346125100C80F33 /* UIColor+Extensions.swift in Sources */,
AEACE2E51FB32E1900DCDD78 /* Utils.swift in Sources */,
3052C60E253F088E007D13EA /* DetectorType.swift in Sources */,
Expand Down
77 changes: 55 additions & 22 deletions deltachat-ios/Chat/ChatViewController.swift
Expand Up @@ -16,8 +16,9 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
var ephemeralTimerModifiedObserver: NSObjectProtocol?
private var isInitial = true
private var isVisibleToUser: Bool = false
private var keepKeyboard: Bool = false
private var keepKeyboard: AtomicBoolean = AtomicBoolean(initialValue: false)
private var wasInputBarFirstResponder = false
private var lastTextViewShouldEndEditingUpdate: Double = 0

lazy var isGroupChat: Bool = {
return dcContext.getChat(chatId: chatId).isGroup
Expand Down Expand Up @@ -256,7 +257,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
imageName: "arrowshape.turn.up.left.fill",
action: #selector(BaseMessageCell.messageReply),
onPerform: { [weak self] indexPath in
self?.keepKeyboard = true
self?.keepKeyboard.set(value: true)
DispatchQueue.main.async { [weak self] in
self?.replyToMessage(at: indexPath)
}
Expand Down Expand Up @@ -366,11 +367,13 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
self.isInitial = false
return
}
logger.debug(">>>> .didChangeFrame")
if self.isLastRowVisible() && !self.tableView.isDragging && !self.tableView.isDecelerating && self.highlightedMsg == nil {
self.scrollToBottom()
}
}.on(event: .willChangeFrame) { [weak self] _ in
guard let self = self else { return }
logger.debug(">>>> .willChangeFrame")
if self.isLastRowVisible() && !self.tableView.isDragging && !self.tableView.isDecelerating && self.highlightedMsg == nil && !self.isInitial {
self.scrollToBottom()
}
Expand All @@ -384,6 +387,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

if dcChat.canSend {
configureUIForWriting()
adaptContentInset(isInitial: true)
} else if dcChat.isContactRequest {
configureContactRequestBar()
} else {
Expand All @@ -392,6 +396,28 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
loadMessages()
}

private func adaptContentInset(isInitial: Bool = false) {
if isInitial {
let fontSize = self.messageInputBar.inputTextView.font.pointSize
let padding = 32.0
let minBottomInset = 52.0
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: max(minBottomInset, fontSize + padding), right: 0)
logger.debug(">>>> initial font size: \(fontSize) bottom: \(max(minBottomInset, fontSize + padding))")
} else {
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: self.messageInputBar.keyboardHeight, right: 0)
}
}

private func resetContentInset() {
let currentBottomOffset = self.tableView.contentInset.vertical
if currentBottomOffset > 0 {
UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) { [weak self] in
self?.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}
logger.debug(">>>> resetContentInset \(0)")
}

private func configureUIForWriting() {
configureMessageInputBar()
draft.parse(draftMsg: dcContext.getDraft(chatId: chatId))
Expand Down Expand Up @@ -561,16 +587,8 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
guard let self = self else { return }
if let ui = notification.userInfo {
logger.debug(">>> msgChangedObserver: \(String(describing: ui["message_id"]))")
if self.dcChat.canSend, let id = ui["message_id"] as? Int, id > 0 {
let msg = self.dcContext.getMessage(id: id)
if msg.isInfo,
let parent = msg.parent,
parent.type == DC_MSG_WEBXDC {
self.refreshMessages()
} else {
self.updateMessage(msg)
}
} else {
if let id = ui["chat_id"] as? Int, id == 0 || // deleted messages or batch insert
id == self.chatId {
self.refreshMessages()
DispatchQueue.main.async {
self.updateScrollDownButtonVisibility()
Expand Down Expand Up @@ -813,16 +831,19 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
markSeenMessagesInVisibleArea()
updateScrollDownButtonVisibility()
}
resetContentInset()
}

public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
markSeenMessagesInVisibleArea()
updateScrollDownButtonVisibility()
resetContentInset()
}

override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
markSeenMessagesInVisibleArea()
updateScrollDownButtonVisibility()
resetContentInset()
}

private func updateScrollDownButtonVisibility() {
Expand Down Expand Up @@ -881,7 +902,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

let action = UIContextualAction(style: .normal, title: nil,
handler: { [weak self] (_, _, completionHandler) in
self?.keepKeyboard = true
self?.keepKeyboard.set(value: true)
self?.replyToMessage(at: indexPath)
completionHandler(true)
})
Expand Down Expand Up @@ -1149,6 +1170,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func scrollToBottom(animated: Bool, focusOnVoiceOver: Bool = false) {
logger.debug(">>>> scrollToBottom animated \(animated)")
if !messageIds.isEmpty {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
Expand Down Expand Up @@ -1711,7 +1733,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func stageDocument(url: NSURL) {
keepKeyboard = true
keepKeyboard.set(value: true)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.draft.setAttachment(viewType: url.pathExtension == "xdc" ? DC_MSG_WEBXDC : DC_MSG_FILE, path: url.relativePath)
Expand All @@ -1722,7 +1744,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func stageVideo(url: NSURL) {
keepKeyboard = true
keepKeyboard.set(value: true)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.draft.setAttachment(viewType: DC_MSG_VIDEO, path: url.relativePath)
Expand All @@ -1733,7 +1755,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}

private func stageImage(url: NSURL) {
keepKeyboard = true
keepKeyboard.set(value: true)
DispatchQueue.global().async { [weak self] in
if let image = ImageFormat.loadImageFrom(url: url as URL) {
self?.stageImage(image)
Expand Down Expand Up @@ -2196,7 +2218,8 @@ extension ChatViewController: MediaPickerDelegate {
// MARK: - MessageInputBarDelegate
extension ChatViewController: InputBarAccessoryViewDelegate {
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
keepKeyboard = true
logger.debug(">>>> inputbar didPressSendbuttonWith text \(text)")
keepKeyboard.set(value: true)
let trimmedText = text.replacingOccurrences(of: "\u{FFFC}", with: "", options: .literal, range: nil)
.trimmingCharacters(in: .whitespacesAndNewlines)
if let filePath = draft.attachment, let viewType = draft.viewType {
Expand Down Expand Up @@ -2227,14 +2250,14 @@ extension ChatViewController: InputBarAccessoryViewDelegate {
// MARK: - DraftPreviewDelegate
extension ChatViewController: DraftPreviewDelegate {
func onCancelQuote() {
keepKeyboard = true
keepKeyboard.set(value: true)
draft.setQuote(quotedMsg: nil)
configureDraftArea(draft: draft)
focusInputTextView()
}

func onCancelAttachment() {
keepKeyboard = true
keepKeyboard.set(value: true)
draft.clearAttachment()
configureDraftArea(draft: draft)
evaluateInputBar(draft: draft)
Expand Down Expand Up @@ -2433,11 +2456,21 @@ extension ChatViewController: AudioControllerDelegate {
// MARK: - UITextViewDelegate
extension ChatViewController: UITextViewDelegate {
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
if keepKeyboard {
if keepKeyboard.get() {
// the event is triggered twice on some devices, we're debouncing this
logger.debug(">>>>> textViewShouldEndEditing - keep keyboard")
let now = Double(Date().timeIntervalSince1970)
logger.debug(">>>> debounce time: \(now - lastTextViewShouldEndEditingUpdate)")
if now - lastTextViewShouldEndEditingUpdate < 0.5 {
return false
}

lastTextViewShouldEndEditingUpdate = now
adaptContentInset()
DispatchQueue.main.async { [weak self] in
self?.messageInputBar.inputTextView.becomeFirstResponder()
}
keepKeyboard = false
self.keepKeyboard.set(value: false)
return false
}
return true
Expand All @@ -2462,7 +2495,7 @@ extension ChatViewController: WebxdcSelectorDelegate {
}

func onWebxdcSelected(msgId: Int) {
keepKeyboard = true
keepKeyboard.set(value: true)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let message = self.dcContext.getMessage(id: msgId)
Expand Down
29 changes: 29 additions & 0 deletions deltachat-ios/Helper/AtomicBoolean.swift
@@ -0,0 +1,29 @@
import Foundation

public class AtomicBoolean {
private var val: UInt8 = 0
public init(initialValue: Bool) {
self.val = (initialValue == false ? 0 : 1)
}


public func set(value: Bool) {
if value {
OSAtomicTestAndSet(7, &val)
} else {
OSAtomicTestAndClear(7, &val)
}
}

public func getAndSet(value: Bool) -> Bool {
if value {
return OSAtomicTestAndSet(7, &val)
} else {
return OSAtomicTestAndClear(7, &val)
}
}

public func get() -> Bool {
return val != 0
}
}