From 706d033132a228963676fa3d9450ee823a83b29d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 3 Sep 2024 18:52:39 +0200 Subject: [PATCH] fix to scroll to last read message This will fix to scroll to the last read message when a chat is opened. Some refactorings were made that are not necessary for the fix (I tried to also show the "Unread messages" hint in the adapter but came to the conclusion this is not a good idea until chatkit is removed. Chatkit doesn't support to add some item in between but only at the end or start which will make it too complicated..) Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/chat/ChatActivity.kt | 90 ++++++++++++------- .../talk/chat/data/ChatMessageRepository.kt | 2 + .../network/OfflineFirstChatRepository.kt | 37 +++++--- .../talk/chat/viewmodels/ChatViewModel.kt | 2 + 4 files changed, 90 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 38481bbd1a0..d37793d6de6 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -832,6 +832,14 @@ class ChatActivity : .collect() } + this.lifecycleScope.launch { + chatViewModel.getLastReadMessageFlow + .onEach { lastRead -> + scrollToAndCenterMessageWithId(lastRead.toString()) + } + .collect() + } + chatViewModel.reactionDeletedViewState.observe(this) { state -> when (state) { is ChatViewModel.ReactionDeletedSuccessState -> { @@ -2059,6 +2067,18 @@ class ChatActivity : } } + private fun scrollToAndCenterMessageWithId(messageId: String) { + adapter?.let { + val position = it.getMessagePositionByIdInReverse(messageId) + if (position != -1) { + layoutManager?.scrollToPositionWithOffset( + position, + binding.messagesListView.height / 2 + ) + } + } + } + private fun writeContactToVcfFile(cursor: Cursor, file: File) { val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY)) val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey) @@ -2492,34 +2512,11 @@ class ChatActivity : private fun processMessagesFromTheFuture(chatMessageList: List) { val newMessagesAvailable = (adapter?.itemCount ?: 0) > 0 && chatMessageList.isNotEmpty() - val insertNewMessagesNotice = if (newMessagesAvailable) { - chatMessageList.any { it.actorId != conversationUser!!.userId } - } else { - false - } - - val scrollToEndOnUpdate = layoutManager?.findFirstVisibleItemPosition() == 0 + val insertNewMessagesNotice = shouldInsertNewMessagesNotice(newMessagesAvailable, chatMessageList) + val scrollToEndOnUpdate = isScrolledToBottom() if (insertNewMessagesNotice) { - val unreadChatMessage = ChatMessage() - unreadChatMessage.jsonMessageId = -1 - unreadChatMessage.actorId = "-1" - unreadChatMessage.timestamp = chatMessageList[0].timestamp - unreadChatMessage.message = context.getString(R.string.nc_new_messages) - adapter?.addToStart(unreadChatMessage, false) - - if (scrollToEndOnUpdate) { - binding.scrollDownButton.visibility = View.GONE - newMessagesCount = 0 - } else { - if (binding.unreadMessagesPopup.isShown) { - newMessagesCount++ - } else { - newMessagesCount = 1 - binding.scrollDownButton.visibility = View.GONE - binding.unreadMessagesPopup.show() - } - } + updateUnreadMessageInfos(chatMessageList, scrollToEndOnUpdate) } for (chatMessage in chatMessageList) { @@ -2544,6 +2541,42 @@ class ChatActivity : } } + private fun isScrolledToBottom() = layoutManager?.findFirstVisibleItemPosition() == 0 + + private fun shouldInsertNewMessagesNotice( + newMessagesAvailable: Boolean, + chatMessageList: List + ) = if (newMessagesAvailable) { + chatMessageList.any { it.actorId != conversationUser!!.userId } + } else { + false + } + + private fun updateUnreadMessageInfos( + chatMessageList: List, + scrollToEndOnUpdate: Boolean + ) { + val unreadChatMessage = ChatMessage() + unreadChatMessage.jsonMessageId = -1 + unreadChatMessage.actorId = "-1" + unreadChatMessage.timestamp = chatMessageList[0].timestamp + unreadChatMessage.message = context.getString(R.string.nc_new_messages) + adapter?.addToStart(unreadChatMessage, false) + + if (scrollToEndOnUpdate) { + binding.scrollDownButton.visibility = View.GONE + newMessagesCount = 0 + } else { + if (binding.unreadMessagesPopup.isShown) { + newMessagesCount++ + } else { + newMessagesCount = 1 + binding.scrollDownButton.visibility = View.GONE + binding.unreadMessagesPopup.show() + } + } + } + private fun processMessagesNotFromTheFuture(chatMessageList: List) { var countGroupedMessages = 0 @@ -2579,10 +2612,7 @@ class ChatActivity : private fun scrollToFirstUnreadMessage() { adapter?.let { - layoutManager?.scrollToPositionWithOffset( - it.getMessagePositionByIdInReverse("-1"), - binding.messagesListView.height / 2 - ) + scrollToAndCenterMessageWithId("-1") } } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt index 049d86ba67b..2680c84864b 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -32,6 +32,8 @@ interface ChatMessageRepository : LifecycleAwareManager { val lastCommonReadFlow: Flow + val lastReadMessageFlow: Flow + fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String) fun loadInitialMessages(withNetworkParams: Bundle): Job diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt index 01174fd24a4..1b0c383a3f5 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -81,6 +81,13 @@ class OfflineFirstChatRepository @Inject constructor( private val _lastCommonReadFlow: MutableSharedFlow = MutableSharedFlow() + override val lastReadMessageFlow: + Flow + get() = _lastReadMessageFlow + + private val _lastReadMessageFlow: + MutableSharedFlow = MutableSharedFlow() + private var newXChatLastCommonRead: Int? = null private var itIsPaused = false private val scope = CoroutineScope(Dispatchers.IO) @@ -114,25 +121,33 @@ class OfflineFirstChatRepository @Inject constructor( sync(withNetworkParams) - Log.d(TAG, "newestMessageId after sync: " + chatDao.getNewestMessageId(internalConversationId)) + val newestMessageId = chatDao.getNewestMessageId(internalConversationId) + Log.d(TAG, "newestMessageId after sync: $newestMessageId") showLast100MessagesBeforeAndEqual( internalConversationId, chatDao.getNewestMessageId(internalConversationId) ) - updateUiForLastCommonRead(200) + + // delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing + // with them (otherwise there is a race condition). + delay(100) + + updateUiForLastCommonRead() + updateUiForLastReadMessage(newestMessageId) initMessagePolling() } - private fun updateUiForLastCommonRead(delay: Long) { + private suspend fun updateUiForLastReadMessage(newestMessageId: Long) { + val scrollToLastRead = conversationModel.lastReadMessage.toLong() < newestMessageId + if (scrollToLastRead) { + _lastReadMessageFlow.emit(conversationModel.lastReadMessage) + } + } + + private fun updateUiForLastCommonRead() { scope.launch { - // delay is a dirty workaround to make sure messages are added to adapter on initial load before setting - // their read status(otherwise there is a race condition between adding messages and setting their read - // status). - if (delay > 0) { - delay(delay) - } newXChatLastCommonRead?.let { _lastCommonReadFlow.emit(it) } @@ -163,7 +178,7 @@ class OfflineFirstChatRepository @Inject constructor( } showLast100MessagesBefore(internalConversationId, beforeMessageId) - updateUiForLastCommonRead(0) + updateUiForLastCommonRead() } override fun initMessagePolling(): Job = @@ -195,7 +210,7 @@ class OfflineFirstChatRepository @Inject constructor( _messageFlow.emit(pair) } - updateUiForLastCommonRead(0) + updateUiForLastCommonRead() val newestMessage = chatDao.getNewestMessageId(internalConversationId).toInt() diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 91e3146e62f..8f317737575 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -122,6 +122,8 @@ class ChatViewModel @Inject constructor( val getLastCommonReadFlow = chatRepository.lastCommonReadFlow + val getLastReadMessageFlow = chatRepository.lastReadMessageFlow + val getConversationFlow = conversationRepository.conversationFlow .onEach { _getRoomViewState.value = GetRoomSuccessState