WIP add options to temp messages

TODO:
check id type --> see TODO "currentTimeMillies fails as id because later on in the model it's not Long but Int!!!!" in OfflineFirstChatRepository.kt

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-12-20 18:06:30 +01:00
parent ec466e58f0
commit 1bfb3ba027
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
14 changed files with 402 additions and 366 deletions

View File

@ -121,21 +121,23 @@ class OutcomingTextMessageViewHolder(itemView: View) :
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
// if (message.sendingFailed) { if (message.isTemporary && !networkMonitor.isOnline.first()) {
// updateStatus(
// R.drawable.baseline_report_problem_24,
// "failed"
// )
// } else
if (message.isTempMessage && !networkMonitor.isOnline.first()) {
updateStatus( updateStatus(
R.drawable.ic_signal_wifi_off_white_24dp, R.drawable.ic_signal_wifi_off_white_24dp,
"offline" "offline"
) )
} else if (message.isTempMessage) { } else if (message.sendingFailed) {
updateStatus(
R.drawable.baseline_report_problem_24,
"failed"
)
binding.bubble.setOnClickListener {
commonMessageInterface.onOpenMessageActionsDialog(message)
}
} else if (message.isTemporary) {
showSendingSpinner() showSendingSpinner()
} else if(message.readStatus == ReadStatus.READ){ } else if(message.readStatus == ReadStatus.READ) {
updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read)) updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read))
} else if(message.readStatus == ReadStatus.SENT) { } else if(message.readStatus == ReadStatus.SENT) {
updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent)) updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent))

View File

@ -68,9 +68,6 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
} else if (holder instanceof SystemMessageViewHolder holderInstance) { } else if (holder instanceof SystemMessageViewHolder holderInstance) {
holderInstance.assignSystemMessageInterface(chatActivity); holderInstance.assignSystemMessageInterface(chatActivity);
} else if (holder instanceof TemporaryMessageViewHolder holderInstance) {
holderInstance.assignTemporaryMessageInterface(chatActivity);
} else if (holder instanceof IncomingDeckCardViewHolder holderInstance) { } else if (holder instanceof IncomingDeckCardViewHolder holderInstance) {
holderInstance.assignCommonMessageInterface(chatActivity); holderInstance.assignCommonMessageInterface(chatActivity);
} else if (holder instanceof OutcomingDeckCardViewHolder holderInstance) { } else if (holder instanceof OutcomingDeckCardViewHolder holderInstance) {

View File

@ -1,13 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.adapters.messages
interface TemporaryMessageInterface {
fun editTemporaryMessage(id: Int, newMessage: String)
fun deleteTemporaryMessage(id: Int)
}

View File

@ -1,206 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.util.Log
import android.view.View
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import autodagger.AutoInjector
import coil.load
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.databinding.ItemTemporaryMessageBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.message.MessageUtils
import com.stfalcon.chatkit.messages.MessagesListAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class TemporaryMessageViewHolder(outgoingView: View, payload: Any) :
MessagesListAdapter.OutcomingMessageViewHolder<ChatMessage>(outgoingView) {
private val binding: ItemTemporaryMessageBinding = ItemTemporaryMessageBinding.bind(outgoingView)
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var context: Context
@Inject
lateinit var messageUtils: MessageUtils
lateinit var temporaryMessageInterface: TemporaryMessageInterface
var isEditing = false
override fun onBind(message: ChatMessage) {
super.onBind(message)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
viewThemeUtils.platform.colorImageView(binding.tempMsgEdit, ColorRole.PRIMARY)
viewThemeUtils.platform.colorImageView(binding.tempMsgDelete, ColorRole.PRIMARY)
binding.bubble.setOnClickListener {
if (binding.tempMsgActions.isVisible) {
binding.tempMsgActions.visibility = View.GONE
} else {
binding.tempMsgActions.visibility = View.VISIBLE
}
}
binding.tempMsgEdit.setOnClickListener {
isEditing = !isEditing
if (isEditing) {
binding.tempMsgEdit.setImageDrawable(
ResourcesCompat.getDrawable(
context.resources,
R.drawable.ic_check,
null
)
)
binding.messageEdit.visibility = View.VISIBLE
binding.messageEdit.requestFocus()
ViewCompat.getWindowInsetsController(binding.root)?.show(WindowInsetsCompat.Type.ime())
binding.messageEdit.setText(binding.messageText.text)
binding.messageText.visibility = View.GONE
} else {
binding.tempMsgEdit.setImageDrawable(
ResourcesCompat.getDrawable(
context.resources,
R.drawable.ic_edit,
null
)
)
binding.messageEdit.visibility = View.GONE
binding.messageText.visibility = View.VISIBLE
val newMessage = binding.messageEdit.text.toString()
message.message = newMessage
temporaryMessageInterface.editTemporaryMessage(message.tempMessageId, newMessage)
}
}
binding.tempMsgDelete.setOnClickListener {
temporaryMessageInterface.deleteTemporaryMessage(message.tempMessageId)
}
// parent message handling
if (message.parentMessageId != null && message.parentMessageId!! > 0) {
processParentMessage(message)
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
} else {
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
}
val bgBubbleColor = bubble.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
val layout = R.drawable.shape_outcoming_message
val bubbleDrawable = DisplayUtils.getMessageSelector(
bgBubbleColor,
ResourcesCompat.getColor(bubble.resources, R.color.transparent, null),
bgBubbleColor,
layout
)
ViewCompat.setBackground(bubble, bubbleDrawable)
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun processParentMessage(message: ChatMessage) {
if (message.parentMessageId != null && !message.isDeleted) {
CoroutineScope(Dispatchers.Main).launch {
try {
val chatActivity = temporaryMessageInterface as ChatActivity
val urlForChatting = ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,
chatActivity.conversationUser?.baseUrl,
chatActivity.roomToken
)
val parentChatMessage = withContext(Dispatchers.IO) {
chatActivity.chatViewModel.getMessageById(
urlForChatting,
chatActivity.currentConversation!!,
message.parentMessageId!!
).first()
}
parentChatMessage.activeUser = message.activeUser
parentChatMessage.imageUrl?.let {
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
val placeholder = ResourcesCompat.getDrawable(
context.resources,
R.drawable.ic_mimetype_image,
null
)
binding.messageQuote.quotedMessageImage.setImageDrawable(placeholder)
binding.messageQuote.quotedMessageImage.load(it) {
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
)
}
} ?: run {
binding.messageQuote.quotedMessageImage.visibility = View.GONE
}
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
binding.messageQuote.quotedMessage.text = messageUtils
.enrichChatReplyMessageText(
binding.messageQuote.quotedMessage.context,
parentChatMessage,
false,
viewThemeUtils
)
viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage)
viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor)
viewThemeUtils.talk.colorOutgoingQuoteBackground(binding.messageQuote.quoteColoredView)
binding.messageQuote.quotedChatMessageView.setOnClickListener {
val chatActivity = temporaryMessageInterface as ChatActivity
chatActivity.jumpToQuotedMessage(parentChatMessage)
}
} catch (e: Exception) {
Log.d(TAG, "Error when processing parent message in view holder", e)
}
}
}
}
fun assignTemporaryMessageInterface(temporaryMessageInterface: TemporaryMessageInterface) {
this.temporaryMessageInterface = temporaryMessageInterface
}
override fun viewDetached() {
// unused atm
}
override fun viewAttached() {
// unused atm
}
override fun viewRecycled() {
// unused atm
}
companion object {
private val TAG = TemporaryMessageViewHolder::class.java.simpleName
}
}

View File

@ -111,8 +111,6 @@ import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.SystemMessageInterface import com.nextcloud.talk.adapters.messages.SystemMessageInterface
import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
import com.nextcloud.talk.adapters.messages.TemporaryMessageInterface
import com.nextcloud.talk.adapters.messages.TemporaryMessageViewHolder
import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
@ -154,6 +152,7 @@ import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog import com.nextcloud.talk.ui.dialog.MessageActionsDialog
import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
import com.nextcloud.talk.ui.dialog.ShowReactionsDialog import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
import com.nextcloud.talk.ui.dialog.TempMessageActionsDialog
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
@ -230,8 +229,7 @@ class ChatActivity :
CommonMessageInterface, CommonMessageInterface,
PreviewMessageInterface, PreviewMessageInterface,
SystemMessageInterface, SystemMessageInterface,
CallStartedMessageInterface, CallStartedMessageInterface {
TemporaryMessageInterface {
var active = false var active = false
@ -600,16 +598,16 @@ class ChatActivity :
// } // }
messageInputViewModel.messageQueueSizeFlow.observe(this) { size -> messageInputViewModel.messageQueueSizeFlow.observe(this) { size ->
if (size == 0) { // if (size == 0) {
var i = 0 // var i = 0
var pos = adapter?.getMessagePositionById(TEMPORARY_MESSAGE_ID_STRING) // var pos = adapter?.getMessagePositionById(TEMPORARY_MESSAGE_ID_STRING)
while (pos != null && pos > -1) { // while (pos != null && pos > -1) {
adapter?.items?.removeAt(pos) // adapter?.items?.removeAt(pos)
i++ // i++
pos = adapter?.getMessagePositionById(TEMPORARY_MESSAGE_ID_STRING) // pos = adapter?.getMessagePositionById(TEMPORARY_MESSAGE_ID_STRING)
} // }
adapter?.notifyDataSetChanged() // adapter?.notifyDataSetChanged()
} // }
} }
this.lifecycleScope.launch { this.lifecycleScope.launch {
@ -1253,18 +1251,18 @@ class ChatActivity :
viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar) viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar)
} }
private fun getLastAdapterId(): Int { // private fun getLastAdapterId(): Int {
var lastId = 0 // var lastId = 0
if (adapter?.items?.size != 0) { // if (adapter?.items?.size != 0) {
val item = adapter?.items?.get(0)?.item // val item = adapter?.items?.get(0)?.item
if (item != null) { // if (item != null) {
lastId = (item as ChatMessage).jsonMessageId // lastId = (item as ChatMessage).jsonMessageId
} else { // } else {
lastId = 0 // lastId = 0
} // }
} // }
return lastId // return lastId
} // }
private fun setupActionBar() { private fun setupActionBar() {
setSupportActionBar(binding.chatToolbar) setSupportActionBar(binding.chatToolbar)
@ -1384,17 +1382,6 @@ class ChatActivity :
R.layout.item_custom_outcoming_preview_message R.layout.item_custom_outcoming_preview_message
) )
messageHolders.registerContentType(
CONTENT_TYPE_TEMP,
TemporaryMessageViewHolder::class.java,
payload,
R.layout.item_temporary_message,
TemporaryMessageViewHolder::class.java,
payload,
R.layout.item_temporary_message,
this
)
messageHolders.registerContentType( messageHolders.registerContentType(
CONTENT_TYPE_SYSTEM_MESSAGE, CONTENT_TYPE_SYSTEM_MESSAGE,
SystemMessageViewHolder::class.java, SystemMessageViewHolder::class.java,
@ -3437,9 +3424,16 @@ class ChatActivity :
private fun openMessageActionsDialog(iMessage: IMessage?) { private fun openMessageActionsDialog(iMessage: IMessage?) {
val message = iMessage as ChatMessage val message = iMessage as ChatMessage
if (hasVisibleItems(message) &&
!isSystemMessage(message) && if (message.isTemporary) {
message.id != TEMPORARY_MESSAGE_ID_STRING TempMessageActionsDialog(
this,
message,
conversationUser,
currentConversation,
).show()
} else if (hasVisibleItems(message) &&
!isSystemMessage(message)
) { ) {
MessageActionsDialog( MessageActionsDialog(
this, this,
@ -4011,30 +4005,6 @@ class ChatActivity :
startACall(false, false) startACall(false, false)
} }
override fun editTemporaryMessage(id: Int, newMessage: String) {
// messageInputViewModel.editQueuedMessage(currentConversation!!.internalId, id, newMessage)
// adapter?.notifyDataSetChanged() // TODO optimize this
}
override fun deleteTemporaryMessage(id: Int) {
// messageInputViewModel.removeFromQueue(currentConversation!!.internalId, id)
// var i = 0
// val max = messageInputViewModel.messageQueueSizeFlow.value?.plus(1)
// for (item in adapter?.items!!) {
// if (i > max!! && max < 1) break
// if (item.item is ChatMessage &&
// (item.item as ChatMessage).isTempMessage &&
// (item.item as ChatMessage).tempMessageId == id
// ) {
// val index = adapter?.items!!.indexOf(item)
// adapter?.items!!.removeAt(index)
// adapter?.notifyItemRemoved(index)
// break
// }
// i++
// }
}
private fun logConversationInfos(methodName: String) { private fun logConversationInfos(methodName: String) {
Log.d(TAG, " |-----------------------------------------------") Log.d(TAG, " |-----------------------------------------------")
Log.d(TAG, " | method: $methodName") Log.d(TAG, " | method: $methodName")

View File

@ -916,16 +916,23 @@ class MessageInputFragment : Fragment() {
// FIXME Fix API checking with guests? // FIXME Fix API checking with guests?
val apiVersion: Int = ApiUtils.getChatApiVersion(chatActivity.spreedCapabilities, intArrayOf(1)) val apiVersion: Int = ApiUtils.getChatApiVersion(chatActivity.spreedCapabilities, intArrayOf(1))
chatActivity.messageInputViewModel.editChatMessage( if (message.isTemporary) {
chatActivity.credentials!!, chatActivity.messageInputViewModel.editTempChatMessage(
ApiUtils.getUrlForChatMessage( message,
apiVersion, editedMessageText
chatActivity.conversationUser!!.baseUrl!!, )
chatActivity.roomToken, } else {
message.id chatActivity.messageInputViewModel.editChatMessage(
), chatActivity.credentials!!,
editedMessageText ApiUtils.getUrlForChatMessage(
) apiVersion,
chatActivity.conversationUser!!.baseUrl!!,
chatActivity.roomToken,
message.id
),
editedMessageText
)
}
} }
private fun setEditUI(message: ChatMessage) { private fun setEditUI(message: ChatMessage) {

View File

@ -14,6 +14,7 @@ import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
interface ChatMessageRepository : LifecycleAwareManager { interface ChatMessageRepository : LifecycleAwareManager {
@ -97,4 +98,6 @@ interface ChatMessageRepository : LifecycleAwareManager {
): Flow<Result<ChatMessage?>> ): Flow<Result<ChatMessage?>>
suspend fun editChatMessage(credentials: String, url: String, text: String): Flow<Result<ChatOverallSingleMessage>> suspend fun editChatMessage(credentials: String, url: String, text: String): Flow<Result<ChatOverallSingleMessage>>
suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean>
} }

View File

@ -115,9 +115,9 @@ data class ChatMessage(
var openWhenDownloaded: Boolean = true, var openWhenDownloaded: Boolean = true,
var isTempMessage: Boolean = false, // TODO: replace logic from message drafts with logic from temp message sending var isTemporary: Boolean = false, // TODO: replace logic from message drafts with logic from temp message sending
var tempMessageId: Int = -1, // TODO: replace logic from message drafts with logic from temp message sending // var tempMessageId: Int = -1, // TODO: replace logic from message drafts with logic from temp message sending
var referenceId: String? = null, var referenceId: String? = null,

View File

@ -13,10 +13,6 @@ import android.util.Log
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel.Companion
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel.SendChatMessageErrorState
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel.SendChatMessageSuccessState
import com.nextcloud.talk.data.database.dao.ChatBlocksDao import com.nextcloud.talk.data.database.dao.ChatBlocksDao
import com.nextcloud.talk.data.database.dao.ChatMessagesDao import com.nextcloud.talk.data.database.dao.ChatMessagesDao
import com.nextcloud.talk.data.database.mappers.asEntity import com.nextcloud.talk.data.database.mappers.asEntity
@ -41,10 +37,13 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class OfflineFirstChatRepository @Inject constructor( class OfflineFirstChatRepository @Inject constructor(
@ -338,32 +337,6 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
// TODO replace with WorkManager?
// private suspend fun tryToSendPendingMessages() {
// val tempMessages = chatDao.getTempMessagesForConversation(internalConversationId).first()
//
// tempMessages.forEach {
// Log.d(TAG, "Sending chat message ${it.message} another time!!")
//
// sendChatMessage(
// credentials,
// urlForChatting,
// it.message,
// it.actorDisplayName,
// it.parentMessageId?.toInt() ?: 0,
// false,
// it.referenceId ?: ""
// ).collect { result ->
// if (result.isSuccess) {
// Log.d(TAG, "success. received ref id: " + (result.getOrNull()?.referenceId ?: "none"))
//
// } else {
// Log.d(TAG, "fail. received ref id: " + (result.getOrNull()?.referenceId ?: "none"))
// }
// }
// }
// }
private suspend fun handleNewAndTempMessages( private suspend fun handleNewAndTempMessages(
receivedChatMessages : List<ChatMessage>, receivedChatMessages : List<ChatMessage>,
lookIntoFuture: Boolean, lookIntoFuture: Boolean,
@ -829,35 +802,42 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId: String referenceId: String
): Flow<Result<ChatMessage?>> = ): Flow<Result<ChatMessage?>> =
flow { flow {
try { val response = network.sendChatMessage(
val response = network.sendChatMessage( credentials,
credentials, url,
url, message,
message, displayName,
displayName, replyTo,
replyTo, sendWithoutNotification,
sendWithoutNotification, referenceId
referenceId )
)
val chatMessageModel = response.ocs?.data?.asModel() val chatMessageModel = response.ocs?.data?.asModel()
emit(Result.success(chatMessageModel)) emit(Result.success(chatMessageModel))
} catch (e: Exception) { }
Log.e(TAG, "Error when sending message", e) // .retryWhen { cause, attempt ->
// if (cause is IOException && attempt < 3) {
// delay(2000)
// return@retryWhen true
// } else {
// return@retryWhen false
// }
// }
.catch { e ->
Log.e(TAG, "Error when sending message", e)
val failedMessage = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first() val failedMessage = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
failedMessage.sendingFailed = true failedMessage.sendingFailed = true
chatDao.updateChatMessage(failedMessage) chatDao.updateChatMessage(failedMessage)
// val failedMessageModel = failedMessage.asModel() val failedMessageModel = failedMessage.asModel()
// _removeMessageFlow.emit(failedMessageModel) _removeMessageFlow.emit(failedMessageModel)
//
// val tripleChatMessages = Triple(true, false, listOf(failedMessageModel))
// _messageFlow.emit(tripleChatMessages)
emit(Result.failure(e)) val tripleChatMessages = Triple(true, false, listOf(failedMessageModel))
} _messageFlow.emit(tripleChatMessages)
emit(Result.failure(e))
} }
override suspend fun editChatMessage( override suspend fun editChatMessage(
@ -878,6 +858,30 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
override suspend fun editTempChatMessage(
message: ChatMessage, editedMessageText: String
): Flow<Boolean> =
flow {
try {
val messageToEdit = chatDao.getChatMessageForConversation(internalConversationId, message.jsonMessageId
.toLong()).first()
messageToEdit.message = editedMessageText
chatDao.upsertChatMessage(messageToEdit)
val editedMessageModel = messageToEdit.asModel()
_removeMessageFlow.emit(editedMessageModel)
val tripleChatMessages = Triple(true, false, listOf(editedMessageModel))
_messageFlow.emit(tripleChatMessages)
emit(true)
} catch (e: Exception) {
emit(false)
}
}
override suspend fun addTemporaryMessage( override suspend fun addTemporaryMessage(
message: CharSequence, message: CharSequence,
displayName: String, displayName: String,
@ -917,7 +921,7 @@ class OfflineFirstChatRepository @Inject constructor(
val entity = ChatMessageEntity( val entity = ChatMessageEntity(
internalId = internalConversationId + "@_temp_" + currentTimeMillies, internalId = internalConversationId + "@_temp_" + currentTimeMillies,
internalConversationId = internalConversationId, internalConversationId = internalConversationId,
id = currentTimeMillies, id = currentTimeMillies, // TODO: currentTimeMillies fails as id because later on in the model it's not Long but Int!!!!
message = message, message = message,
deleted = false, deleted = false,
token = conversationModel.token, token = conversationModel.token,

View File

@ -219,6 +219,21 @@ class MessageInputViewModel @Inject constructor(
} }
} }
fun editTempChatMessage(message: ChatMessage, editedMessageText: String) {
viewModelScope.launch {
chatRepository.editTempChatMessage(
message,
editedMessageText
).collect { result ->
if (true) {
// _editMessageViewState.value = EditMessageSuccessState(result)
} else {
// _editMessageViewState.value = EditMessageErrorState
}
}
}
}
fun reply(message: IMessage?) { fun reply(message: IMessage?) {
_getReplyChatMessage.postValue(message) _getReplyChatMessage.postValue(message)
} }

View File

@ -32,6 +32,7 @@ interface ChatMessagesDao {
SELECT * SELECT *
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 0
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
""" """
) )
@ -78,10 +79,10 @@ interface ChatMessagesDao {
@Query( @Query(
value = """ value = """
DELETE FROM ChatMessages DELETE FROM ChatMessages
WHERE id in (:messageIds) WHERE internalId in (:internalIds)
""" """
) )
fun deleteChatMessages(messageIds: List<Int>) fun deleteChatMessages(internalIds: List<String>)
@Query( @Query(
value = """ value = """

View File

@ -66,7 +66,7 @@ fun ChatMessageEntity.asModel() =
lastEditTimestamp = lastEditTimestamp, lastEditTimestamp = lastEditTimestamp,
isDeleted = deleted, isDeleted = deleted,
referenceId = referenceId, referenceId = referenceId,
isTempMessage = isTemporary, isTemporary = isTemporary,
sendingFailed = sendingFailed, sendingFailed = sendingFailed,
readStatus = setStatus(isTemporary, sendingFailed) readStatus = setStatus(isTemporary, sendingFailed)
) )

View File

@ -0,0 +1,124 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2022 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.ui.dialog
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.DialogMessageActionsBinding
import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding
import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils
import kotlinx.coroutines.launch
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class TempMessageActionsDialog(
private val chatActivity: ChatActivity,
private val message: ChatMessage,
private val user: User?,
private val currentConversation: ConversationModel?
) : BottomSheetDialog(chatActivity) {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var dateUtils: DateUtils
@Inject
lateinit var networkMonitor: NetworkMonitor
private lateinit var binding: DialogTempMessageActionsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
binding = DialogTempMessageActionsBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
viewThemeUtils.material.colorBottomSheetBackground(binding.root)
viewThemeUtils.material.colorBottomSheetDragHandle(binding.bottomSheetDragHandle)
initMenuItemCopy(!message.isDeleted)
val apiVersion = ApiUtils.getConversationApiVersion(user!!, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
initMenuItems()
}
private fun initMenuItems() {
this.lifecycleScope.launch {
initMenuEditMessage(true)
initMenuDeleteMessage(true)
}
}
override fun onStart() {
super.onStart()
val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
val behavior = BottomSheetBehavior.from(bottomSheet as View)
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
private fun initMenuDeleteMessage(visible: Boolean) {
if (visible) {
binding.menuDeleteMessage.setOnClickListener {
chatActivity.deleteMessage(message)
dismiss()
}
}
binding.menuDeleteMessage.visibility = getVisibility(visible)
}
private fun initMenuEditMessage(visible: Boolean) {
binding.menuEditMessage.setOnClickListener {
chatActivity.messageInputViewModel.edit(message)
dismiss()
}
binding.menuEditMessage.visibility = getVisibility(visible)
}
private fun initMenuItemCopy(visible: Boolean) {
if (visible) {
binding.menuCopyMessage.setOnClickListener {
chatActivity.copyMessage(message)
dismiss()
}
}
binding.menuCopyMessage.visibility = getVisibility(visible)
}
private fun getVisibility(visible: Boolean): Int {
return if (visible) {
View.VISIBLE
} else {
View.GONE
}
}
companion object {
private val TAG = TempMessageActionsDialog::class.java.simpleName
}
}

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/standard_half_padding">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/bottom_sheet_drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/message_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/menu_copy_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_copy_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/ic_content_copy"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_copy_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/nc_copy_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_edit_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_edit_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/edit_message_icon_description"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/ic_edit_24"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_edit_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/nc_edit_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_delete_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_delete_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/ic_delete"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_delete_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/nc_delete_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>