diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt index b056ec0d8..804d2d093 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt @@ -114,7 +114,19 @@ class OutcomingTextMessageViewHolder(itemView: View) : binding.messageQuote.quotedChatMessageView.visibility = View.GONE } - setReadStatus(message.readStatus) + + when (message.readStatus) { + ReadStatus.READ -> updateReadStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read)) + ReadStatus.SENT -> updateReadStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent)) + ReadStatus.SENDING -> updateSendingStatus() + ReadStatus.FAILED -> updateReadStatus( + R.drawable.ic_baseline_close_24, + "failed" + ) + else -> null + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) @@ -129,29 +141,27 @@ class OutcomingTextMessageViewHolder(itemView: View) : ) } - private fun setReadStatus(readStatus: Enum) { - val readStatusDrawableInt = when (readStatus) { - ReadStatus.READ -> R.drawable.ic_check_all - ReadStatus.SENT -> R.drawable.ic_check - else -> null - } - - val readStatusContentDescriptionString = when (readStatus) { - ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read) - ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent) - else -> null - } - - readStatusDrawableInt?.let { drawableInt -> + private fun updateReadStatus(readStatusDrawableInt: Int, description: String?) { + binding.sendingProgress.visibility = View.GONE + binding.checkMark.visibility = View.VISIBLE + readStatusDrawableInt.let { drawableInt -> ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let { binding.checkMark.setImageDrawable(it) viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) } } - - binding.checkMark.contentDescription = readStatusContentDescriptionString + binding.checkMark.contentDescription = description } + private fun updateSendingStatus() { + binding.sendingProgress.visibility = View.VISIBLE + binding.checkMark.visibility = View.GONE + + viewThemeUtils.material.colorProgressBar(binding.sendingProgress) + } + + + private fun longClickOnReaction(chatMessage: ChatMessage) { commonMessageInterface.onLongClickReactions(chatMessage) } 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 d7ca12def..e786832bc 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -587,7 +587,7 @@ class ChatActivity : list.forEachIndexed { _, qMsg -> val temporaryChatMessage = ChatMessage() temporaryChatMessage.jsonMessageId = TEMPORARY_MESSAGE_ID_INT - temporaryChatMessage.actorId = "-3" + temporaryChatMessage.actorId = TEMPORARY_MESSAGE_ID_STRING temporaryChatMessage.timestamp = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS temporaryChatMessage.message = qMsg.message.toString() temporaryChatMessage.tempMessageId = qMsg.id @@ -813,6 +813,8 @@ class ChatActivity : } is MessageInputViewModel.SendChatMessageErrorState -> { + binding.messagesListView.smoothScrollToPosition(0) + // if (state.e is HttpException) { // val code = state.e.code() // if (code.toString().startsWith("2")) { @@ -2933,7 +2935,11 @@ class ChatActivity : if (message.item is ChatMessage) { val chatMessage = message.item as ChatMessage - if (chatMessage.jsonMessageId <= xChatLastCommonRead) { + if (chatMessage.sendingFailed) { + chatMessage.readStatus = ReadStatus.FAILED + } else if (chatMessage.isTempMessage) { + chatMessage.readStatus = ReadStatus.SENDING + } else if (chatMessage.jsonMessageId <= xChatLastCommonRead) { chatMessage.readStatus = ReadStatus.READ } else { chatMessage.readStatus = ReadStatus.SENT @@ -3439,7 +3445,7 @@ class ChatActivity : val message = iMessage as ChatMessage if (hasVisibleItems(message) && !isSystemMessage(message) && - message.id != "-3" + message.id != TEMPORARY_MESSAGE_ID_STRING ) { MessageActionsDialog( this, @@ -3863,7 +3869,7 @@ class ChatActivity : CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == UNREAD_MESSAGES_MARKER_ID.toString() CONTENT_TYPE_CALL_STARTED -> message.id == "-2" - CONTENT_TYPE_TEMP -> message.id == "-3" + CONTENT_TYPE_TEMP -> message.id == TEMPORARY_MESSAGE_ID_STRING CONTENT_TYPE_DECK_CARD -> message.isDeckCard() else -> false diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index 37bc3b213..0e50ef18a 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -115,11 +115,13 @@ data class ChatMessage( var openWhenDownloaded: Boolean = true, - var isTempMessage: Boolean = false, + var isTempMessage: Boolean = false, // TODO: replace logic from message drafts with logic from temp message sending - var tempMessageId: Int = -1, + var tempMessageId: Int = -1, // TODO: replace logic from message drafts with logic from temp message sending - var referenceId: String? = null + var referenceId: String? = null, + + var sendingFailed: Boolean = true ) : MessageContentType, MessageContentType.Image { 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 1f8001fd0..7db771a2b 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 @@ -186,7 +186,7 @@ class OfflineFirstChatRepository @Inject constructor( ) if (list.isNotEmpty()) { updateUiMessages( - chatMessages = list, + receivedChatMessages = list, lookIntoFuture = false, showUnreadMessagesMarker = false ) @@ -308,7 +308,7 @@ class OfflineFirstChatRepository @Inject constructor( showUnreadMessagesMarker = showUnreadMessagesMarker && !weHaveMessagesFromOurself updateUiMessages( - chatMessages = chatMessages, + receivedChatMessages = chatMessages, lookIntoFuture = true, showUnreadMessagesMarker = showUnreadMessagesMarker ) @@ -335,7 +335,7 @@ class OfflineFirstChatRepository @Inject constructor( } private suspend fun updateUiMessages( - chatMessages : List, + receivedChatMessages : List, lookIntoFuture: Boolean, showUnreadMessagesMarker: Boolean ) { @@ -346,11 +346,11 @@ class OfflineFirstChatRepository @Inject constructor( oldTempMessages.forEach { _removeMessageFlow.emit(it) } // add new messages to UI - val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, chatMessages) + val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages) _messageFlow.emit(tripleChatMessages) // remove temp messages from DB that are now found in the new messages - val chatMessagesReferenceIds = chatMessages.mapTo(HashSet(chatMessages.size)) { it.referenceId } + val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId } val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds } chatDao.deleteTempChatMessages( internalConversationId, @@ -815,6 +815,17 @@ class OfflineFirstChatRepository @Inject constructor( emit(Result.success(chatMessageModel)) } catch (e: Exception) { Log.e(TAG, "Error when sending message", e) + + val failedMessage = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first() + failedMessage.sendingFailed = true + chatDao.updateChatMessage(failedMessage) + + val failedMessageModel = failedMessage.asModel() + _removeMessageFlow.emit(failedMessageModel) + + val tripleChatMessages = Triple(true, false, listOf(failedMessageModel)) + _messageFlow.emit(tripleChatMessages) + emit(Result.failure(e)) } } @@ -877,7 +888,7 @@ class OfflineFirstChatRepository @Inject constructor( internalId = internalConversationId + "@_temp_" + currentTimeMillies, internalConversationId = internalConversationId, id = currentTimeMillies, - message = message + " (temp)", + message = message, deleted = false, token = conversationModel.token, actorId = currentUser.userId!!, diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt index a15cdbe1c..b3179c149 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt @@ -48,6 +48,18 @@ interface ChatMessagesDao { ) fun getTempMessagesForConversation(internalConversationId: String): Flow> + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND referenceId = :referenceId + AND isTemporary = 1 + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertChatMessages(chatMessages: List) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt index 4ab3c4955..1392a2920 100644 --- a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt @@ -10,6 +10,7 @@ package com.nextcloud.talk.data.database.mappers import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.data.database.model.ChatMessageEntity import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.models.json.chat.ReadStatus fun ChatMessageJson.asEntity(accountId: Long) = ChatMessageEntity( @@ -64,9 +65,22 @@ fun ChatMessageEntity.asModel() = lastEditActorType = lastEditActorType, lastEditTimestamp = lastEditTimestamp, isDeleted = deleted, - referenceId = referenceId + referenceId = referenceId, + isTempMessage = isTemporary, + sendingFailed = sendingFailed, + readStatus = setStatus(isTemporary, sendingFailed) ) +fun setStatus(isTemporary: Boolean, sendingFailed: Boolean): ReadStatus { + return if (sendingFailed) { + ReadStatus.FAILED + } else if (isTemporary) { + ReadStatus.SENDING + } else { + ReadStatus.NONE + } +} + fun ChatMessageJson.asModel() = ChatMessage( jsonMessageId = id.toInt(), diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt index 40a1e283c..1441dd521 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt @@ -9,5 +9,7 @@ package com.nextcloud.talk.models.json.chat enum class ReadStatus { NONE, SENT, - READ + READ, + SENDING, + FAILED } diff --git a/app/src/main/res/layout/item_custom_outcoming_text_message.xml b/app/src/main/res/layout/item_custom_outcoming_text_message.xml index 4d0d2d452..7c93c5d2a 100644 --- a/app/src/main/res/layout/item_custom_outcoming_text_message.xml +++ b/app/src/main/res/layout/item_custom_outcoming_text_message.xml @@ -84,6 +84,32 @@ app:layout_alignSelf="center" app:tint="@color/high_emphasis_text" /> + + + +