This commit is contained in:
Arshak Aghakaryan 2023-09-03 20:34:49 +04:00 committed by GitHub
commit 35c51bc922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 87 additions and 35 deletions

View File

@ -1005,6 +1005,8 @@ extension ConversationVC:
}
self.scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight)
let originalMessageTimestamp = Interaction.TimestampInfo(id: cellViewModel.id, timestampMs: cellViewModel.timestampMs)
self.replyNavigationStack.add(originalMessageTimestamp)
}
else if let linkPreview: LinkPreview = cellViewModel.linkPreview {
switch linkPreview.variant {

View File

@ -53,6 +53,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
var didFinishInitialLayout = false
var scrollDistanceToBottomBeforeUpdate: CGFloat?
var baselineKeyboardHeight: CGFloat = 0
var replyNavigationStack = ConversationReplyNavigationStack()
/// These flags are true between `viewDid/Will Appear/Disappear` and is used to prevent keyboard changes
/// from trying to animate (as the animations can cause buggy transitions)
@ -246,13 +247,17 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
image: UIImage(named: "ic_chevron_down")?
.withRenderingMode(.alwaysTemplate)
) { [weak self] in
// The table view's content size is calculated by the estimated height of cells,
// so the result may be inaccurate before all the cells are loaded. Use this
// to scroll to the last row instead.
self?.scrollToBottom(isAnimated: true)
if let previousPosition = self?.replyNavigationStack.removeLast() {
self?.scrollToInteractionIfNeeded(with: previousPosition, focusBehaviour: .highlight)
} else {
// The table view's content size is calculated by the estimated height of cells,
// so the result may be inaccurate before all the cells are loaded. Use this
// to scroll to the last row instead.
self?.scrollToBottom(isAnimated: true)
}
}
result.alpha = 0
return result
}()
@ -1640,6 +1645,13 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
isUserScrolling = false
if !decelerate {
self.onScrollFinished()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.onScrollFinished()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -1669,6 +1681,16 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
self?.markFullyVisibleAndOlderCellsAsRead(interactionInfo: focusedInteractionInfo)
self?.highlightCellIfNeeded(interactionId: focusedInteractionInfo.id, behaviour: behaviour)
}
self.onScrollFinished()
}
func onScrollFinished() {
DispatchQueue.main.async { [weak self] in
if let newestMessageViewModel: MessageViewModel = self?.getNewestMessageViewModel() {
self?.replyNavigationStack.removeTimestampsOlder(than: newestMessageViewModel.timestampMs)
}
}
}
func updateUnreadCountView(unreadCount: UInt?) {
@ -1935,36 +1957,9 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}
func markFullyVisibleAndOlderCellsAsRead(interactionInfo: Interaction.TimestampInfo?) {
// We want to mark messages as read on load and while we scroll, so grab the newest message and mark
// everything older as read
//
// Note: For the 'tableVisualBottom' we remove the 'Values.mediumSpacing' as that is the distance
// the table content appears above the input view
let tableVisualBottom: CGFloat = (tableView.frame.maxY - (tableView.contentInset.bottom - Values.mediumSpacing))
guard
let visibleIndexPaths: [IndexPath] = self.tableView.indexPathsForVisibleRows,
let messagesSection: Int = visibleIndexPaths
.first(where: { self.viewModel.interactionData[$0.section].model == .messages })?
.section,
let newestCellViewModel: MessageViewModel = visibleIndexPaths
.sorted()
.filter({ $0.section == messagesSection })
.compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in
guard let cell: VisibleMessageCell = tableView.cellForRow(at: indexPath) as? VisibleMessageCell else {
return nil
}
return (
view.convert(cell.frame, from: tableView),
self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
)
})
// Exclude messages that are partially off the bottom of the screen
.filter({ $0.frame.maxY <= tableVisualBottom })
.last?
.cellViewModel
else {
// We want to mark messages as read on load and while we scroll,
// so grab the newest message and mark everything older as read
guard let newestCellViewModel = getNewestMessageViewModel() else {
// If we weren't able to get any visible cells for some reason then we should fall back to
// marking the provided interactionInfo as read just in case
if let interactionInfo: Interaction.TimestampInfo = interactionInfo {
@ -2001,9 +1996,64 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}
}
func getNewestMessageViewModel() -> MessageViewModel? {
// Note: For the 'tableVisualBottom' we remove the 'Values.mediumSpacing' as that is the distance
// the table content appears above the input view
let tableVisualBottom: CGFloat = (tableView.frame.maxY - (tableView.contentInset.bottom - Values.mediumSpacing))
let visibleIndexPaths: [IndexPath]? = self.tableView.indexPathsForVisibleRows
let messagesSection: Int? = visibleIndexPaths?.first(where: { self.viewModel.interactionData[$0.section].model == .messages })?.section
let newestCellViewModel: MessageViewModel? = visibleIndexPaths?
.sorted()
.filter { $0.section == messagesSection }
.compactMap { indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in
guard let cell: VisibleMessageCell = tableView.cellForRow(at: indexPath) as? VisibleMessageCell else {
return nil
}
return (
view.convert(cell.frame, from: tableView),
self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
)
}
// Exclude messages that are partially off the bottom of the screen
.filter { $0.frame.maxY <= tableVisualBottom }
.last?
.cellViewModel
return newestCellViewModel
}
// MARK: - SessionUtilRespondingViewController
func isConversation(in threadIds: [String]) -> Bool {
return threadIds.contains(self.viewModel.threadData.threadId)
}
}
struct ConversationReplyNavigationStack {
private var messageTimestamps: [Interaction.TimestampInfo] = []
mutating func add(_ timestamp: Interaction.TimestampInfo) {
if !messageTimestamps.contains(where: { $0.id == timestamp.id }) {
self.messageTimestamps.append(timestamp)
}
}
mutating func removeLast() -> Interaction.TimestampInfo? {
if isEmpty { return nil }
return messageTimestamps.removeLast()
}
var isEmpty: Bool { messageTimestamps.isEmpty }
mutating func removeTimestampsOlder(than timestamp: Int64) {
for i in (0 ..< self.messageTimestamps.count).reversed() {
if self.messageTimestamps[i].timestampMs <= timestamp {
self.messageTimestamps.remove(at: i)
}
}
}
}