diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index 1ce44b40a..c1a22a94e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Date import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -77,13 +78,10 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : override fun onBind(message: ChatMessage) { super.onBind(message) sharedApplication!!.componentApplication.inject(this) - setAvatarAndAuthorOnMessageItem(message) colorizeMessageBubble(message) - itemView.isSelected = false val user = currentUserProvider.currentUser.blockingGet() - val hasCheckboxes = processCheckboxes( message, user @@ -122,40 +120,46 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) binding.messageText.text = processedMessageText - if (message.lastEditTimestamp != 0L && !message.isDeleted) { - binding.messageEditIndicator.visibility = View.VISIBLE - binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) - } else { - binding.messageEditIndicator.visibility = View.GONE - binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) - } - - // parent message handling - if (!message.isDeleted && message.parentMessageId != null) { - processParentMessage(message) - binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE - } else { - binding.messageQuote.quotedChatMessageView.visibility = View.GONE - } - - itemView.setTag(R.string.replyable_message_view_tag, message.replyable) - - Reaction().showReactions( - message, - ::clickOnReaction, - ::longClickOnReaction, - binding.reactions, - binding.messageText.context, - false, - viewThemeUtils - ) + }else{ + binding.messageText.text = "" } + + if (message.lastEditTimestamp != 0L && !message.isDeleted) { + binding.messageEditIndicator.visibility = View.VISIBLE + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) + } else { + binding.messageEditIndicator.visibility = View.GONE + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + } + // parent message handling + if (!message.isDeleted && message.parentMessageId != null) { + processParentMessage(message) + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageText.context, + false, + viewThemeUtils + ) } private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean { + val chatActivity = commonMessageInterface as ChatActivity val message = chatMessage.message!!.toSpanned() val messageTextView = binding.messageText val checkBoxContainer = binding.checkboxContainer + val isOlderThanTwentyFourHours = chatMessage + .createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) checkBoxContainer.removeAllViews() val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) @@ -178,6 +182,8 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : val checkBox = CheckBox(checkBoxContainer.context).apply { text = taskText this.isChecked = isChecked + this.isEnabled = !isOlderThanTwentyFourHours && chatMessage.actorType == "bots" || chatActivity + .userAllowedByPrivilages(chatMessage) setOnCheckedChangeListener { _, _ -> updateCheckboxStates(chatMessage, user, checkboxList) } @@ -186,7 +192,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : checkboxList.add(checkBox) viewThemeUtils.platform.themeCheckbox(checkBox) } - checkBoxContainer.visibility = View.VISIBLE return true } @@ -347,5 +352,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : private val TAG = IncomingTextMessageViewHolder::class.java.simpleName private const val CHECKED_GROUP_INDEX = 2 private const val TASK_TEXT_GROUP_INDEX = 3 + private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000 } } 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 907b3cd1d..6e54d5259 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 @@ -13,31 +13,39 @@ import android.content.Context import android.util.Log import android.util.TypedValue import android.view.View +import android.widget.CheckBox import androidx.core.content.res.ResourcesCompat +import androidx.core.text.toSpanned import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import coil.load import com.google.android.flexbox.FlexboxLayout +import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.ChatMessageRepository 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.ItemCustomOutcomingTextMessageBinding import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.TextMatchers +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.message.MessageUtils import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Date import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -65,30 +73,32 @@ class OutcomingTextMessageViewHolder(itemView: View) : lateinit var commonMessageInterface: CommonMessageInterface + @Inject + lateinit var chatRepository: ChatMessageRepository + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + private var job: Job? = null + @Suppress("Detekt.LongMethod") override fun onBind(message: ChatMessage) { super.onBind(message) sharedApplication!!.componentApplication.inject(this) + val user = currentUserProvider.currentUser.blockingGet() + val hasCheckboxes = processCheckboxes( + message, + user + ) + processMessage(message, hasCheckboxes) + } + + private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) { realView.isSelected = false val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams layoutParams.isWrapBefore = false - var textSize = context.resources.getDimension(R.dimen.chat_text_size) viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) - - var processedMessageText = messageUtils.enrichChatMessageText( - binding.messageText.context, - message, - false, - viewThemeUtils - ) - processedMessageText = messageUtils.processMessageParameters( - binding.messageText.context, - viewThemeUtils, - processedMessageText!!, - message, - itemView - ) - + var textSize = context.resources.getDimension(R.dimen.chat_text_size) var isBubbled = true if ( (message.messageParameters == null || message.messageParameters!!.size <= 0) && @@ -105,7 +115,25 @@ class OutcomingTextMessageViewHolder(itemView: View) : binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) binding.messageTime.layoutParams = layoutParams viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT) - binding.messageText.text = processedMessageText + + if (!hasCheckboxes) { + var processedMessageText = messageUtils.enrichChatMessageText( + binding.messageText.context, + message, + false, + viewThemeUtils + ) + processedMessageText = messageUtils.processMessageParameters( + binding.messageText.context, + viewThemeUtils, + processedMessageText!!, + message, + itemView + ) + binding.messageText.text = processedMessageText + }else{ + binding.messageText.text = "" + } if (message.lastEditTimestamp != 0L && !message.isDeleted) { binding.messageEditIndicator.visibility = View.VISIBLE @@ -161,6 +189,94 @@ class OutcomingTextMessageViewHolder(itemView: View) : ) } + private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean { + val message = chatMessage.message!!.toSpanned() + val messageTextView = binding.messageText + val checkBoxContainer = binding.checkboxContainer + val isOlderThanTwentyFourHours = chatMessage + .createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) + + checkBoxContainer.removeAllViews() + val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) + val matches = regex.findAll(message) + + if (matches.none()) return false + + val firstPart = message.toString().substringBefore("\n- [") + messageTextView.text = messageUtils.enrichChatMessageText( + binding.messageText.context, firstPart, true, viewThemeUtils + ) + + val checkboxList = mutableListOf() + + matches.forEach { matchResult -> + val isChecked = matchResult.groupValues[CHECKED_GROUP_INDEX] == "X" || + matchResult.groupValues[CHECKED_GROUP_INDEX] == "x" + val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim() + + val checkBox = CheckBox(checkBoxContainer.context).apply { + text = taskText + this.isChecked = isChecked + this.isEnabled = !isOlderThanTwentyFourHours + setOnCheckedChangeListener { _, _ -> + updateCheckboxStates(chatMessage, user, checkboxList) + } + } + checkBoxContainer.addView(checkBox) + checkboxList.add(checkBox) + viewThemeUtils.platform.themeCheckbox(checkBox) + } + + checkBoxContainer.visibility = View.VISIBLE + return true + } + + private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List) { + job = CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + val apiVersion: Int = ApiUtils.getChatApiVersion( + user.capabilities?.spreedCapability!!, + intArrayOf(1) + ) + val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes) + chatRepository.editChatMessage( + user.getCredentials(), + ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id), + updatedMessage + ).collect { result -> + withContext(Dispatchers.Main) { + if (result.isSuccess) { + val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!! + Log.d(TAG, "EditedMessage: $editedMessage") + binding.messageEditIndicator.apply { + visibility = View.VISIBLE + } + binding.messageTime.text = + dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!) + } else { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + } + } + } + } + + private fun updateMessageWithCheckboxStates(originalMessage: String, checkboxes: List): String { + var updatedMessage = originalMessage + val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) + + checkboxes.forEach { checkBox -> + updatedMessage = regex.replace(updatedMessage) { matchResult -> + val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim() + val checkboxState = if (checkboxes.find { it.text == taskText }?.isChecked == true) "X" else " " + "- [$checkboxState] $taskText" + } + } + return updatedMessage + } + private fun updateStatus(readStatusDrawableInt: Int, description: String?) { binding.sendingProgress.visibility = View.GONE binding.checkMark.visibility = View.VISIBLE @@ -248,5 +364,8 @@ class OutcomingTextMessageViewHolder(itemView: View) : companion object { const val TEXT_SIZE_MULTIPLIER = 2.5 private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName + private const val CHECKED_GROUP_INDEX = 2 + private const val TASK_TEXT_GROUP_INDEX = 3 + private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000 } } 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 ec70f5e47..ffe06be8b 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 @@ -43,6 +43,17 @@ android:textIsSelectable="false" tools:text="Talk to you later!" /> + + + +