Merge pull request #4136 from nextcloud/bugfix/noid/fixUnreadMessageScrollOnEnterChat

fix to scroll to last read message
This commit is contained in:
Marcel Hibbe 2024-09-04 09:49:24 +02:00 committed by GitHub
commit 822af2c967
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 41 deletions

View File

@ -832,6 +832,14 @@ class ChatActivity :
.collect() .collect()
} }
this.lifecycleScope.launch {
chatViewModel.getLastReadMessageFlow
.onEach { lastRead ->
scrollToAndCenterMessageWithId(lastRead.toString())
}
.collect()
}
chatViewModel.reactionDeletedViewState.observe(this) { state -> chatViewModel.reactionDeletedViewState.observe(this) { state ->
when (state) { when (state) {
is ChatViewModel.ReactionDeletedSuccessState -> { 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) { private fun writeContactToVcfFile(cursor: Cursor, file: File) {
val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY)) val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey) val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
@ -2492,34 +2512,11 @@ class ChatActivity :
private fun processMessagesFromTheFuture(chatMessageList: List<ChatMessage>) { private fun processMessagesFromTheFuture(chatMessageList: List<ChatMessage>) {
val newMessagesAvailable = (adapter?.itemCount ?: 0) > 0 && chatMessageList.isNotEmpty() val newMessagesAvailable = (adapter?.itemCount ?: 0) > 0 && chatMessageList.isNotEmpty()
val insertNewMessagesNotice = if (newMessagesAvailable) { val insertNewMessagesNotice = shouldInsertNewMessagesNotice(newMessagesAvailable, chatMessageList)
chatMessageList.any { it.actorId != conversationUser!!.userId } val scrollToEndOnUpdate = isScrolledToBottom()
} else {
false
}
val scrollToEndOnUpdate = layoutManager?.findFirstVisibleItemPosition() == 0
if (insertNewMessagesNotice) { if (insertNewMessagesNotice) {
val unreadChatMessage = ChatMessage() updateUnreadMessageInfos(chatMessageList, scrollToEndOnUpdate)
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()
}
}
} }
for (chatMessage in chatMessageList) { for (chatMessage in chatMessageList) {
@ -2544,6 +2541,42 @@ class ChatActivity :
} }
} }
private fun isScrolledToBottom() = layoutManager?.findFirstVisibleItemPosition() == 0
private fun shouldInsertNewMessagesNotice(
newMessagesAvailable: Boolean,
chatMessageList: List<ChatMessage>
) = if (newMessagesAvailable) {
chatMessageList.any { it.actorId != conversationUser!!.userId }
} else {
false
}
private fun updateUnreadMessageInfos(
chatMessageList: List<ChatMessage>,
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<ChatMessage>) { private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
var countGroupedMessages = 0 var countGroupedMessages = 0
@ -2579,10 +2612,7 @@ class ChatActivity :
private fun scrollToFirstUnreadMessage() { private fun scrollToFirstUnreadMessage() {
adapter?.let { adapter?.let {
layoutManager?.scrollToPositionWithOffset( scrollToAndCenterMessageWithId("-1")
it.getMessagePositionByIdInReverse("-1"),
binding.messagesListView.height / 2
)
} }
} }

View File

@ -32,6 +32,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
val lastCommonReadFlow: Flow<Int> val lastCommonReadFlow: Flow<Int>
val lastReadMessageFlow: Flow<Int>
fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String) fun setData(conversationModel: ConversationModel, credentials: String, urlForChatting: String)
fun loadInitialMessages(withNetworkParams: Bundle): Job fun loadInitialMessages(withNetworkParams: Bundle): Job

View File

@ -81,6 +81,13 @@ class OfflineFirstChatRepository @Inject constructor(
private val _lastCommonReadFlow: private val _lastCommonReadFlow:
MutableSharedFlow<Int> = MutableSharedFlow() MutableSharedFlow<Int> = MutableSharedFlow()
override val lastReadMessageFlow:
Flow<Int>
get() = _lastReadMessageFlow
private val _lastReadMessageFlow:
MutableSharedFlow<Int> = MutableSharedFlow()
private var newXChatLastCommonRead: Int? = null private var newXChatLastCommonRead: Int? = null
private var itIsPaused = false private var itIsPaused = false
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@ -114,25 +121,33 @@ class OfflineFirstChatRepository @Inject constructor(
sync(withNetworkParams) sync(withNetworkParams)
Log.d(TAG, "newestMessageId after sync: " + chatDao.getNewestMessageId(internalConversationId)) val newestMessageId = chatDao.getNewestMessageId(internalConversationId)
Log.d(TAG, "newestMessageId after sync: $newestMessageId")
showLast100MessagesBeforeAndEqual( showLast100MessagesBeforeAndEqual(
internalConversationId, internalConversationId,
chatDao.getNewestMessageId(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() initMessagePolling()
} }
private fun updateUiForLastCommonRead(delay: Long) { private suspend fun updateUiForLastReadMessage(newestMessageId: Long) {
scope.launch { val scrollToLastRead = conversationModel.lastReadMessage.toLong() < newestMessageId
// delay is a dirty workaround to make sure messages are added to adapter on initial load before setting if (scrollToLastRead) {
// their read status(otherwise there is a race condition between adding messages and setting their read _lastReadMessageFlow.emit(conversationModel.lastReadMessage)
// status).
if (delay > 0) {
delay(delay)
} }
}
private fun updateUiForLastCommonRead() {
scope.launch {
newXChatLastCommonRead?.let { newXChatLastCommonRead?.let {
_lastCommonReadFlow.emit(it) _lastCommonReadFlow.emit(it)
} }
@ -163,7 +178,7 @@ class OfflineFirstChatRepository @Inject constructor(
} }
showLast100MessagesBefore(internalConversationId, beforeMessageId) showLast100MessagesBefore(internalConversationId, beforeMessageId)
updateUiForLastCommonRead(0) updateUiForLastCommonRead()
} }
override fun initMessagePolling(): Job = override fun initMessagePolling(): Job =
@ -195,7 +210,7 @@ class OfflineFirstChatRepository @Inject constructor(
_messageFlow.emit(pair) _messageFlow.emit(pair)
} }
updateUiForLastCommonRead(0) updateUiForLastCommonRead()
val newestMessage = chatDao.getNewestMessageId(internalConversationId).toInt() 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_OK: Int = 200
private const val HTTP_CODE_NOT_MODIFIED = 304 private const val HTTP_CODE_NOT_MODIFIED = 304
private const val HTTP_CODE_PRECONDITION_FAILED = 412 private const val HTTP_CODE_PRECONDITION_FAILED = 412
private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100
} }
} }

View File

@ -122,6 +122,8 @@ class ChatViewModel @Inject constructor(
val getLastCommonReadFlow = chatRepository.lastCommonReadFlow val getLastCommonReadFlow = chatRepository.lastCommonReadFlow
val getLastReadMessageFlow = chatRepository.lastReadMessageFlow
val getConversationFlow = conversationRepository.conversationFlow val getConversationFlow = conversationRepository.conversationFlow
.onEach { .onEach {
_getRoomViewState.value = GetRoomSuccessState _getRoomViewState.value = GetRoomSuccessState