Add scroll to bottom & unread chat message counter in chat room while scrolling up in history
This commit is contained in:
parent
76bf525244
commit
338c136778
9 changed files with 130 additions and 5 deletions
|
@ -22,6 +22,7 @@ Group changes to describe their impact on the project, as follows:
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
|
- 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
|
- 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)
|
- 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
|
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
|
||||||
|
|
|
@ -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.
|
// True if we are still waiting for the last set of data to load.
|
||||||
private var loading = true
|
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.
|
// 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,
|
// 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.
|
// 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
|
previousTotalItemCount = totalItemCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1
|
||||||
|
if (userHasScrolledUp) {
|
||||||
|
onScrolledUp()
|
||||||
|
} else {
|
||||||
|
onScrolledToEnd()
|
||||||
|
}
|
||||||
|
|
||||||
// If it isn’t currently loading, we check to see if we have breached
|
// If it isn’t currently loading, we check to see if we have breached
|
||||||
// the mVisibleThreshold and need to reload more data.
|
// the mVisibleThreshold and need to reload more data.
|
||||||
// If we do need to reload some more data, we execute onLoadMore to fetch the 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
|
// Defines the process for actually loading more data based on page
|
||||||
protected abstract fun onLoadMore(totalItemsCount: Int)
|
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 {
|
companion object {
|
||||||
// The minimum amount of items to have below your current scroll position
|
// The minimum amount of items to have below your current scroll position
|
||||||
// before loading more.
|
// before loading more.
|
||||||
|
|
|
@ -75,8 +75,15 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
|
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
|
||||||
|
|
||||||
|
// Scroll to newly added messages automatically
|
||||||
if (positionStart == adapter.itemCount - itemCount) {
|
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)
|
RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT, swipeConfiguration, swipeListener)
|
||||||
.attachToRecyclerView(binding.chatMessagesList)*/
|
.attachToRecyclerView(binding.chatMessagesList)*/
|
||||||
|
|
||||||
val chatScrollListener: ChatScrollListener = object : ChatScrollListener(layoutManager) {
|
val chatScrollListener = object : ChatScrollListener(layoutManager) {
|
||||||
override fun onLoadMore(totalItemsCount: Int) {
|
override fun onLoadMore(totalItemsCount: Int) {
|
||||||
listViewModel.loadMoreData(totalItemsCount)
|
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)
|
binding.chatMessagesList.addOnScrollListener(chatScrollListener)
|
||||||
|
|
||||||
|
@ -440,6 +457,10 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.setScrollToBottomClickListener {
|
||||||
|
smoothScrollToPosition()
|
||||||
|
}
|
||||||
|
|
||||||
if (textToShare?.isNotEmpty() == true) {
|
if (textToShare?.isNotEmpty() == true) {
|
||||||
Log.i("[Chat Room] Found text to share")
|
Log.i("[Chat Room] Found text to share")
|
||||||
chatSendingViewModel.textToSend.value = textToShare
|
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() {
|
private fun pickFile() {
|
||||||
val intentsList = ArrayList<Intent>()
|
val intentsList = ArrayList<Intent>()
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.max
|
||||||
import org.linphone.activities.main.chat.data.EventLogData
|
import org.linphone.activities.main.chat.data.EventLogData
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
@ -212,7 +213,10 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
|
|
||||||
private fun getEvents(): ArrayList<EventLogData> {
|
private fun getEvents(): ArrayList<EventLogData> {
|
||||||
val list = arrayListOf<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) {
|
for (eventLog in history) {
|
||||||
list.add(EventLogData(eventLog))
|
list.add(EventLogData(eventLog))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,13 @@
|
||||||
*/
|
*/
|
||||||
package org.linphone.activities.main.chat.viewmodels
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.contact.Contact
|
import org.linphone.contact.Contact
|
||||||
import org.linphone.contact.ContactDataInterface
|
import org.linphone.contact.ContactDataInterface
|
||||||
|
@ -81,12 +84,29 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
MutableLiveData<Boolean>()
|
MutableLiveData<Boolean>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isUserScrollingUp = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
var oneParticipantOneDevice: Boolean = false
|
var oneParticipantOneDevice: Boolean = false
|
||||||
|
|
||||||
var addressToCall: Address? = null
|
var addressToCall: Address? = null
|
||||||
|
|
||||||
var onlyParticipantOnlyDeviceAddress: 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() {
|
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||||
override fun onContactsUpdated() {
|
override fun onContactsUpdated() {
|
||||||
Log.i("[Chat Room] Contacts have changed")
|
Log.i("[Chat Room] Contacts have changed")
|
||||||
|
@ -202,6 +222,8 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
|
|
||||||
callInProgress.value = chatRoom.core.callsNb > 0
|
callInProgress.value = chatRoom.core.callsNb > 0
|
||||||
updateRemotesComposing()
|
updateRemotesComposing()
|
||||||
|
|
||||||
|
if (corePreferences.enableAnimations) bounceAnimator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -213,6 +235,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||||
chatRoom.removeListener(chatRoomListener)
|
chatRoom.removeListener(chatRoomListener)
|
||||||
chatRoom.core.removeListener(coreListener)
|
chatRoom.core.removeListener(coreListener)
|
||||||
|
if (corePreferences.enableAnimations) bounceAnimator.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideMenu(): Boolean {
|
fun hideMenu(): Boolean {
|
||||||
|
|
|
@ -142,8 +142,8 @@ class NotificationsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentlyDisplayedChatRoomAddress == room.peerAddress.asStringUriOnly()) {
|
if (currentlyDisplayedChatRoomAddress == room.peerAddress.asStringUriOnly()) {
|
||||||
Log.i("[Notifications Manager] Chat room is currently displayed, do not notify received message & mark it as read")
|
Log.i("[Notifications Manager] Chat room is currently displayed, do not notify received message")
|
||||||
room.markAsRead()
|
// Mark as read is now done in the DetailChatRoomFragment
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
app/src/main/res/drawable-xhdpi/scroll_to_bottom_default.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/scroll_to_bottom_default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
20
app/src/main/res/drawable/scroll_to_bottom.xml
Normal file
20
app/src/main/res/drawable/scroll_to_bottom.xml
Normal 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>
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
<variable
|
<variable
|
||||||
name="voiceRecordingTouchListener"
|
name="voiceRecordingTouchListener"
|
||||||
type="android.view.View.OnTouchListener" />
|
type="android.view.View.OnTouchListener" />
|
||||||
|
<variable
|
||||||
|
name="scrollToBottomClickListener"
|
||||||
|
type="android.view.View.OnClickListener"/>
|
||||||
<variable
|
<variable
|
||||||
name="viewModel"
|
name="viewModel"
|
||||||
type="org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel" />
|
type="org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel" />
|
||||||
|
@ -297,6 +300,38 @@
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginRight="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>
|
</RelativeLayout>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|
Loading…
Reference in a new issue