Only load 30 messages when opening conversation, loading more messages when scrolling up

This commit is contained in:
Sylvain Berfini 2024-01-23 11:19:04 +01:00
parent b919f51ecb
commit ef47624b9d
3 changed files with 162 additions and 14 deletions

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
internal abstract class ConversationScrollListener(private val mLayoutManager: LinearLayoutManager) :
RecyclerView.OnScrollListener() {
companion object {
// The minimum amount of items to have below your current scroll position
// before loading more.
private const val mVisibleThreshold = 5
}
// The total number of items in the data set after the last load
private var previousTotalItemCount = 0
// True if we are still waiting for the last set of data to load.
private var loading = true
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
val totalItemCount = mLayoutManager.itemCount
val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition()
val lastVisibleItemPosition: Int = mLayoutManager.findLastCompletelyVisibleItemPosition()
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
previousTotalItemCount = totalItemCount
if (totalItemCount == 0) {
loading = true
}
}
// If its still loading, we check to see if the data set count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && totalItemCount > previousTotalItemCount) {
loading = false
previousTotalItemCount = totalItemCount
}
val userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1
if (userHasScrolledUp) {
onScrolledUp()
} else {
onScrolledToEnd()
}
// If it isnt currently loading, we check to see if we have breached
// the mVisibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading &&
firstVisibleItemPosition < mVisibleThreshold &&
firstVisibleItemPosition >= 0 &&
lastVisibleItemPosition < totalItemCount - mVisibleThreshold
) {
onLoadMore(totalItemCount)
loading = true
}
}
// Defines the process for actually loading more data based on page
protected abstract fun onLoadMore(totalItemsCount: Int)
// Called when user has started to scroll up, opposed to onScrolledToEnd()
protected abstract fun onScrolledUp()
// Called when user has scrolled and reached the end of the items
protected abstract fun onScrolledToEnd()
}

View file

@ -51,9 +51,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.tabs.TabLayout
@ -72,6 +70,7 @@ import org.linphone.databinding.ChatBubbleLongPressMenuBinding
import org.linphone.databinding.ChatConversationFragmentBinding
import org.linphone.databinding.ChatConversationPopupMenuBinding
import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.ConversationScrollListener
import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter
import org.linphone.ui.main.chat.model.MessageDeliveryModel
@ -177,6 +176,10 @@ class ConversationFragment : SlidingPaneChildFragment() {
private val dataObserver = object : AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart > 0) {
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
}
if (viewModel.isUserScrollingUp.value == true) {
Log.i(
"$TAG [$itemCount] events have been loaded but user was scrolling up in conversation, do not scroll"
@ -218,6 +221,8 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
}
private lateinit var scrollListener: ConversationScrollListener
private var currentChatMessageModelForBottomSheet: MessageModel? = null
private val bottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
@ -616,17 +621,25 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
}
binding.eventsList.addOnScrollListener(object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val scrollingUp = layoutManager.findLastCompletelyVisibleItemPosition() != adapter.itemCount - 1
viewModel.isUserScrollingUp.value = scrollingUp
if (!scrollingUp) {
Log.i("$TAG Last message is visible, considering conversation as read")
viewModel.markAsRead()
}
scrollListener = object : ConversationScrollListener(layoutManager) {
@UiThread
override fun onLoadMore(totalItemsCount: Int) {
viewModel.loadMoreData(totalItemsCount)
}
})
@UiThread
override fun onScrolledUp() {
viewModel.isUserScrollingUp.value = true
}
@UiThread
override fun onScrolledToEnd() {
viewModel.isUserScrollingUp.value = false
Log.i("$TAG Last message is visible, considering conversation as read")
viewModel.markAsRead()
}
}
binding.eventsList.addOnScrollListener(scrollListener)
}
override fun onResume() {
@ -651,6 +664,10 @@ class ConversationFragment : SlidingPaneChildFragment() {
override fun onPause() {
super.onPause()
if (::scrollListener.isInitialized) {
binding.eventsList.removeOnScrollListener(scrollListener)
}
coreContext.postOnCoreThread {
bottomSheetReactionsModel?.destroy()
bottomSheetDeliveryModel?.destroy()

View file

@ -45,6 +45,7 @@ import org.linphone.utils.LinphoneUtils
class ConversationViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Conversation ViewModel]"
private const val MESSAGES_PER_PAGE = 30
const val MAX_TIME_TO_GROUP_MESSAGES = 60 // 1 minute
const val SCROLLING_POSITION_NOT_SET = -1
@ -158,7 +159,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
)
val lastEvent = eventsList.lastOrNull()
val newEvent = newList.lastOrNull()
val newEvent = newList.firstOrNull()
if (lastEvent != null && newEvent != null && shouldWeGroupTwoEvents(
newEvent.eventLog,
lastEvent.eventLog
@ -462,6 +463,43 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
}
}
@UiThread
fun loadMoreData(totalItemsCount: Int) {
coreContext.postOnCoreThread {
val maxSize: Int = chatRoom.historyEventsSize
Log.i("$TAG Loading more data, current total is $totalItemsCount, max size is $maxSize")
if (totalItemsCount < maxSize) {
var upperBound: Int = totalItemsCount + MESSAGES_PER_PAGE
if (upperBound > maxSize) {
upperBound = maxSize
}
val history = chatRoom.getHistoryRangeEvents(totalItemsCount, upperBound)
val list = getEventsListFromHistory(history, searchFilter.value.orEmpty())
val lastEvent = list.lastOrNull()
val newEvent = eventsList.firstOrNull()
if (lastEvent != null && newEvent != null && shouldWeGroupTwoEvents(
newEvent.eventLog,
lastEvent.eventLog
)
) {
if (lastEvent.model is MessageModel) {
lastEvent.model.groupedWithNextMessage.postValue(true)
}
if (newEvent.model is MessageModel) {
newEvent.model.groupedWithPreviousMessage.postValue(true)
}
}
list.addAll(eventsList)
eventsList = list
events.postValue(eventsList)
}
}
}
@WorkerThread
private fun configureChatRoom() {
scrollingPosition = SCROLLING_POSITION_NOT_SET
@ -524,7 +562,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
private fun computeEvents(filter: String = "") {
eventsList.forEach(EventLogModel::destroy)
val history = chatRoom.getHistoryEvents(0)
val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE)
val list = getEventsListFromHistory(history, filter)
eventsList = list
events.postValue(eventsList)