From ef47624b9dec8c9c9ec8c408569102bda69802a6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 23 Jan 2024 11:19:04 +0100 Subject: [PATCH] Only load 30 messages when opening conversation, loading more messages when scrolling up --- .../main/chat/ConversationScrollListener.kt | 93 +++++++++++++++++++ .../chat/fragment/ConversationFragment.kt | 41 +++++--- .../chat/viewmodel/ConversationViewModel.kt | 42 ++++++++- 3 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt diff --git a/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt b/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt new file mode 100644 index 000000000..44bd06329 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt @@ -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 . + */ +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 it’s 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 isn’t 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() +} diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 7a98fcae2..e938cea68 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -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() diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 2aa5eedb1..ab578015b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -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)