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 38481bbd1a..d37793d6de 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 049d86ba67..2680c84864 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 01174fd24a..6a15e5d098 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(DELAY_TO_ENSURE_MESSAGES_ARE_ADDED) + + 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() @@ -612,5 +627,6 @@ class OfflineFirstChatRepository @Inject constructor( private const val HTTP_CODE_OK: Int = 200 private const val HTTP_CODE_NOT_MODIFIED = 304 private const val HTTP_CODE_PRECONDITION_FAILED = 412 + private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100 } } 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 91e3146e62..8f31773757 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