Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -827,4 +827,9 @@ extension TransactionMessage {
}
return groupedMessages
}

static func getCountOfMessages(forChat chat: Chat, fromId messageId: Int) -> Int {
let predicate = NSPredicate(format: "chat == %@ AND id >= %d", chat, messageId)
return CoreDataManager.sharedManager.getObjectsCountOfTypeWith(predicate: predicate, entityName: "TransactionMessage")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class NewChatViewController: NewKeyboardHandlerViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// Save scroll state before the view disappears
chatTableDataSource?.saveSnapshotCurrentState()

if self.isMovingFromParent {
chat?.setChatMessagesAsSeen()
}
Expand All @@ -154,7 +157,6 @@ class NewChatViewController: NewKeyboardHandlerViewController {
super.viewDidDisappear(animated)

if self.isMovingFromParent {
chatTableDataSource?.saveSnapshotCurrentState()
chatTableDataSource?.stopListeningToResultsController()

stopPlayingClip()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,13 @@ class MessagesPreloaderHelper {
}

struct ScrollState {
var bottomFirstVisibleRow: Int
var bottomFirstVisibleRowOffset: CGFloat
var bottomFirstVisibleRowUniqueID: Int?
var numberOfItems: Int
var shouldAdjustScroll: Bool
var shouldPreventSetMessagesAsSeen: Bool

init(
bottomFirstVisibleRow: Int,
bottomFirstVisibleRowOffset: CGFloat,
bottomFirstVisibleRowUniqueID: Int?,
numberOfItems: Int,
shouldAdjustScroll: Bool,
shouldPreventSetMessagesAsSeen: Bool
) {
self.bottomFirstVisibleRow = bottomFirstVisibleRow
self.bottomFirstVisibleRowOffset = bottomFirstVisibleRowOffset
self.bottomFirstVisibleRowUniqueID = bottomFirstVisibleRowUniqueID
self.numberOfItems = numberOfItems
self.shouldAdjustScroll = shouldAdjustScroll
self.shouldPreventSetMessagesAsSeen = shouldPreventSetMessagesAsSeen
}
var firstRowId: Int // message.id of the topmost visible row
var difference: CGFloat // pixel offset of that row from the top of the viewport
var isAtBottom: Bool
}

var chatMessages: [Int: [MessageTableCellState]] = [:]
var chatLastPositions: [Int: ScrollState] = [:]
var chatScrollState: [Int: ScrollState] = [:]

var tribesData: [String: MessageTableCellState.TribeData] = [:]
var linksData: [String: MessageTableCellState.LinkData] = [:]
Expand All @@ -67,47 +48,18 @@ class MessagesPreloaderHelper {
return nil
}

func save(
bottomFirstVisibleRow: Int,
bottomFirstVisibleRowOffset: CGFloat,
bottomFirstVisibleRowUniqueID: Int?,
numberOfItems: Int,
for chatId: Int
) {
self.chatLastPositions[chatId] = ScrollState(
bottomFirstVisibleRow: bottomFirstVisibleRow,
bottomFirstVisibleRowOffset: bottomFirstVisibleRowOffset,
bottomFirstVisibleRowUniqueID: bottomFirstVisibleRowUniqueID,
numberOfItems: numberOfItems,
shouldAdjustScroll: true,
shouldPreventSetMessagesAsSeen: true
)
func save(firstRowId: Int, difference: CGFloat, isAtBottom: Bool, for chatId: Int) {
chatScrollState[chatId] = ScrollState(firstRowId: firstRowId, difference: difference, isAtBottom: isAtBottom)
}

func getScrollState(
for chatId: Int,
with newItemIdentifiers: [MessageTableCellState]
) -> ScrollState? {

if let scrollState = chatLastPositions[chatId] {

if let firstItemBeforeUpdate = scrollState.bottomFirstVisibleRowUniqueID {

let itemUniqueIdentifiers = newItemIdentifiers.map({ $0.getUniqueIdentifier() })
let difference = itemUniqueIdentifiers.index(of: firstItemBeforeUpdate) ?? 0
let destinationRow = scrollState.bottomFirstVisibleRow + difference
let shouldAdjustScroll = destinationRow > 1 || (destinationRow > 0 && scrollState.bottomFirstVisibleRowOffset > 0)

return ScrollState(
bottomFirstVisibleRow: destinationRow,
bottomFirstVisibleRowOffset: scrollState.bottomFirstVisibleRowOffset,
bottomFirstVisibleRowUniqueID: scrollState.bottomFirstVisibleRowUniqueID,
numberOfItems: scrollState.numberOfItems,
shouldAdjustScroll: shouldAdjustScroll,
shouldPreventSetMessagesAsSeen: scrollState.numberOfItems == newItemIdentifiers.count
)
}
func getScrollState(for chatId: Int, pinnedMessageId: Int? = nil) -> ScrollState? {
if let pinnedMessageId = pinnedMessageId {
return ScrollState(firstRowId: pinnedMessageId, difference: 0, isAtBottom: false)
}
return nil
return chatScrollState[chatId]
}

func reset(for chatId: Int) {
chatScrollState.removeValue(forKey: chatId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,74 +34,67 @@ extension NewChatTableDataSource {
}

@objc func saveSnapshotCurrentState() {
guard let chat = chat else {
return
}
// Do not overwrite a saved state that hasn't been consumed yet
guard !isRestoringScrollPosition else { return }
guard let chatId = chat?.id else { return }

if let firstVisibleRow = tableView.indexPathsForVisibleRows?.first {

let cellRectInTable = tableView.rectForRow(at: firstVisibleRow)
let cellOffset = tableView.convert(cellRectInTable.origin, to: bottomView)

preloaderHelper.save(
bottomFirstVisibleRow: firstVisibleRow.row,
bottomFirstVisibleRowOffset: cellOffset.y,
bottomFirstVisibleRowUniqueID: dataSource.snapshot().itemIdentifiers.first?.getUniqueIdentifier(),
numberOfItems: preloaderHelper.getPreloadedMessagesCount(for: chat.id),
for: chat.id
)
}
// In the inverted UITableView, .last = highest index = topmost visible row on screen
guard var topIndexPath = tableView.indexPathsForVisibleRows?.last else { return }

saveMessagesToPreloader()
// Skip date-separator rows (message == nil); walk toward lower index (newer, but still visible)
while topIndexPath.row >= 0 {
let state = messageTableCellStateArray[topIndexPath.row]
if let messageId = state.message?.id {
// Found a real message row — compute pixel offset
let isAtBottom = tableView.contentOffset.y <= Constants.kChatTableContentInset
if isAtBottom {
preloaderHelper.save(firstRowId: messageId, difference: 0, isAtBottom: true, for: chatId)
} else {
let cellRect = tableView.rectForRow(at: topIndexPath)
let cellRectInView = tableView.convert(cellRect, to: tableView)
let visibleTop = tableView.contentOffset.y + tableView.contentInset.top
let difference = cellRectInView.origin.y - visibleTop
preloaderHelper.save(firstRowId: messageId, difference: difference, isAtBottom: false, for: chatId)
}
return
}
if topIndexPath.row == 0 { break }
topIndexPath = IndexPath(row: topIndexPath.row - 1, section: topIndexPath.section)
}
}

@objc func restoreScrollLastPosition() {
guard let chat = chat else {
return
}

guard let chatId = chat?.id else { return }
tableView.alpha = 1.0

if let pinnedMessageId = pinnedMessageId {
if let index = getTableCellStateFor(
messageId: pinnedMessageId,
and: nil
)?.0 {
tableView.scrollToRow(
at: IndexPath(row: index, section: 0),
at: .top,
animated: true
)
if let index = getTableCellStateFor(messageId: pinnedMessageId, and: nil)?.0 {
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .top, animated: true)
}
isRestoringScrollPosition = false
return
}

if let scrollState = preloaderHelper.getScrollState(for: chatId, pinnedMessageId: nil),
!scrollState.isAtBottom {
// Find the row whose message.id matches the saved anchor
if let index = messageTableCellStateArray.firstIndex(where: { $0.message?.id == scrollState.firstRowId }) {
let rowCount = tableView.numberOfRows(inSection: 0)
guard index < rowCount else {
isRestoringScrollPosition = false
delegate?.didScrollToBottom()
return
}
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .top, animated: false)
// Apply saved pixel offset from viewport top
tableView.contentOffset.y = tableView.contentOffset.y - scrollState.difference
isRestoringScrollPosition = false
return
}
}
// else {
// if let scrollState = preloaderHelper.getScrollState(
// for: chat.id,
// with: dataSource.snapshot().itemIdentifiers
// ) {
// let row = scrollState.bottomFirstVisibleRow
// let offset = scrollState.bottomFirstVisibleRowOffset
//
// if scrollState.shouldAdjustScroll && !loadingMoreItems {
//
// if tableView.numberOfRows(inSection: 0) > row {
//
// tableView.scrollToRow(
// at: IndexPath(row: row, section: 0),
// at: .top,
// animated: false
// )
//
// tableView.contentOffset.y = tableView.contentOffset.y + (offset + tableView.contentInset.top)
// }
// }
//
// if scrollState.shouldPreventSetMessagesAsSeen {
// return
// }
// }
// }

// Fallback: scroll to bottom (most recent messages)
isRestoringScrollPosition = false
if tableView.contentOffset.y <= Constants.kChatTableContentInset {
delegate?.didScrollToBottom()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ extension NewChatTableDataSource {

self.saveSnapshotCurrentState()
self.dataSource.apply(snapshot, animatingDifferences: false)
self.isRestoringScrollPosition = true
self.restoreScrollLastPosition()

DelayPerformedHelper.performAfterDelay(seconds: 1.0, completion: {
Expand Down Expand Up @@ -793,19 +794,31 @@ extension NewChatTableDataSource : NSFetchedResultsControllerDelegate {
return
}

messagesCountRequested = items
var requestedItems = items

// If there is a saved scroll state mid-chat, ensure we fetch enough items to cover the anchor
if let chatId = chat.id,
let scrollState = preloaderHelper.getScrollState(for: chatId),
!scrollState.isAtBottom {
// Count how many messages exist from the anchor downward (more recent)
let anchorId = scrollState.firstRowId
let countFromAnchor = TransactionMessage.getCountOfMessages(forChat: chat, fromId: anchorId) + 20
requestedItems = max(items, countFromAnchor)
}

messagesCountRequested = requestedItems

var fetchRequest = getFetchRequestFor(
chat: chat,
with: items
with: requestedItems
)

if let minIndex = getFetchMinIndex(fetchRequest: fetchRequest), !isThread {
fetchMinIndex = minIndex

fetchRequest = getFetchRequestFor(
chat: chat,
with: items,
with: requestedItems,
and: minIndex
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class NewChatTableDataSource : NSObject {
let messageBubbleHelper = NewMessageBubbleHelper()
let audioPlayerHelper = AudioPlayerHelper()
var podcastPlayerController = PodcastPlayerController.sharedInstance
var isRestoringScrollPosition: Bool = false

///Messages Data
var messagesArray: [TransactionMessage] = []
Expand Down