Add scroll to bottom & unread chat message counter in chat room while scrolling up in history

This commit is contained in:
Sylvain Berfini 2021-10-19 17:25:04 +02:00
parent 76bf525244
commit 338c136778
9 changed files with 130 additions and 5 deletions

View file

@ -22,6 +22,7 @@ Group changes to describe their impact on the project, as follows:
### Changed
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it
- Animations have been replaced to use com.google.android.material.transition ones
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2

View file

@ -29,6 +29,8 @@ internal abstract class ChatScrollListener(private val mLayoutManager: LinearLay
// True if we are still waiting for the last set of data to load.
private var loading = true
var userHasScrolledUp: Boolean = false
// 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.
@ -54,6 +56,13 @@ internal abstract class ChatScrollListener(private val mLayoutManager: LinearLay
previousTotalItemCount = totalItemCount
}
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.
@ -67,6 +76,12 @@ internal abstract class ChatScrollListener(private val mLayoutManager: LinearLay
// 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()
companion object {
// The minimum amount of items to have below your current scroll position
// before loading more.

View file

@ -75,8 +75,15 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
// Scroll to newly added messages automatically
if (positionStart == adapter.itemCount - itemCount) {
scrollToBottom()
// But only if user hasn't initiated a scroll up in the messages history
if (viewModel.isUserScrollingUp.value == false) {
scrollToBottom()
viewModel.chatRoom.markAsRead()
} else {
Log.w("[Chat Room] User has scrolled up manually in the messages history, don't scroll to the newly added message at the bottom & don't mark the chat room as read")
}
}
}
}
@ -191,10 +198,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT, swipeConfiguration, swipeListener)
.attachToRecyclerView(binding.chatMessagesList)*/
val chatScrollListener: ChatScrollListener = object : ChatScrollListener(layoutManager) {
val chatScrollListener = object : ChatScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int) {
listViewModel.loadMoreData(totalItemsCount)
}
override fun onScrolledUp() {
viewModel.isUserScrollingUp.value = true
}
override fun onScrolledToEnd() {
viewModel.isUserScrollingUp.value = false
Log.i("[Chat Room] User has scrolled to the latest message, mark chat room as read")
viewModel.chatRoom.markAsRead()
}
}
binding.chatMessagesList.addOnScrollListener(chatScrollListener)
@ -440,6 +457,10 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
false
}
binding.setScrollToBottomClickListener {
smoothScrollToPosition()
}
if (textToShare?.isNotEmpty() == true) {
Log.i("[Chat Room] Found text to share")
chatSendingViewModel.textToSend.value = textToShare
@ -715,6 +736,12 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
}
}
private fun smoothScrollToPosition() {
if (_adapter != null && adapter.itemCount > 0) {
binding.chatMessagesList.smoothScrollToPosition(adapter.itemCount - 1)
}
}
private fun pickFile() {
val intentsList = ArrayList<Intent>()

View file

@ -23,6 +23,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import java.util.*
import kotlin.math.max
import org.linphone.activities.main.chat.data.EventLogData
import org.linphone.core.*
import org.linphone.core.tools.Log
@ -212,7 +213,10 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
private fun getEvents(): ArrayList<EventLogData> {
val list = arrayListOf<EventLogData>()
val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE)
val unreadCount = chatRoom.unreadMessagesCount
val loadCount = max(MESSAGES_PER_PAGE, unreadCount)
Log.i("[Chat Messages] $unreadCount unread messages in this chat room, loading $loadCount from history")
val history = chatRoom.getHistoryEvents(loadCount)
for (eventLog in history) {
list.add(EventLogData(eventLog))
}

View file

@ -19,10 +19,13 @@
*/
package org.linphone.activities.main.chat.viewmodels
import android.animation.ValueAnimator
import android.view.animation.LinearInterpolator
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.contact.Contact
import org.linphone.contact.ContactDataInterface
@ -81,12 +84,29 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
MutableLiveData<Boolean>()
}
val isUserScrollingUp = MutableLiveData<Boolean>()
var oneParticipantOneDevice: Boolean = false
var addressToCall: Address? = null
var onlyParticipantOnlyDeviceAddress: Address? = null
val chatUnreadCountTranslateY = MutableLiveData<Float>()
private val bounceAnimator: ValueAnimator by lazy {
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
addUpdateListener {
val value = it.animatedValue as Float
chatUnreadCountTranslateY.value = value
}
interpolator = LinearInterpolator()
duration = 250
repeatMode = ValueAnimator.REVERSE
repeatCount = ValueAnimator.INFINITE
}
}
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
override fun onContactsUpdated() {
Log.i("[Chat Room] Contacts have changed")
@ -202,6 +222,8 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
callInProgress.value = chatRoom.core.callsNb > 0
updateRemotesComposing()
if (corePreferences.enableAnimations) bounceAnimator.start()
}
override fun onCleared() {
@ -213,6 +235,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
coreContext.contactsManager.removeListener(contactsUpdatedListener)
chatRoom.removeListener(chatRoomListener)
chatRoom.core.removeListener(coreListener)
if (corePreferences.enableAnimations) bounceAnimator.end()
}
fun hideMenu(): Boolean {

View file

@ -142,8 +142,8 @@ class NotificationsManager(private val context: Context) {
}
if (currentlyDisplayedChatRoomAddress == room.peerAddress.asStringUriOnly()) {
Log.i("[Notifications Manager] Chat room is currently displayed, do not notify received message & mark it as read")
room.markAsRead()
Log.i("[Notifications Manager] Chat room is currently displayed, do not notify received message")
// Mark as read is now done in the DetailChatRoomFragment
return
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<bitmap android:src="@drawable/scroll_to_bottom_default"
android:tint="?attr/drawableTintOverColor"/>
</item>
<item android:state_enabled="false">
<bitmap android:src="@drawable/scroll_to_bottom_default"
android:tint="?attr/drawableTintDisabledColor"/>
</item>
<item android:state_selected="true">
<bitmap android:src="@drawable/scroll_to_bottom_default"
android:tint="?attr/drawableTintOverColor"/>
</item>
<item>
<bitmap android:src="@drawable/scroll_to_bottom_default"
android:tint="?attr/drawableTintColor"/>
</item>
</selector>

View file

@ -26,6 +26,9 @@
<variable
name="voiceRecordingTouchListener"
type="android.view.View.OnTouchListener" />
<variable
name="scrollToBottomClickListener"
type="android.view.View.OnClickListener"/>
<variable
name="viewModel"
type="org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel" />
@ -297,6 +300,38 @@
android:layout_marginTop="8dp"
android:layout_marginRight="8dp" />
<RelativeLayout
android:onClick="@{scrollToBottomClickListener}"
android:visibility="@{viewModel.isUserScrollingUp ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_above="@id/footer"
android:layout_alignParentEnd="true">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:background="@drawable/round_button_background"
android:padding="13dp"
android:src="@drawable/scroll_to_bottom" />
<TextView
style="@style/unread_count_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:background="@{viewModel.unreadMessagesCount == 0 ? @drawable/hidden_unread_message_count_bg : @drawable/unread_message_count_bg, default=@drawable/unread_message_count_bg}"
android:gravity="center"
android:text="@{viewModel.unreadMessagesCount == 0 ? `` : String.valueOf(viewModel.unreadMessagesCount), default=1}"
android:translationY="@{viewModel.chatUnreadCountTranslateY}" />
</RelativeLayout>
</RelativeLayout>
</layout>