From 78883efe7f0b4bbf4e914e59726a7cab25b41c7d Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 25 Feb 2026 22:26:40 +0100 Subject: [PATCH 1/2] feat(chat): Double tap on message to react Resolves #5240 Signed-off-by: Andy Scherzinger --- .../com/nextcloud/talk/chat/ChatActivity.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 8b5aaf80fe..3488623013 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -29,6 +29,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler +import android.os.SystemClock import android.provider.ContactsContract import android.provider.MediaStore import android.provider.Settings @@ -39,6 +40,7 @@ import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.WindowManager import android.view.animation.AccelerateDecelerateInterpolator @@ -263,6 +265,7 @@ class ChatActivity : MessagesListAdapter.OnLoadMoreListener, MessagesListAdapter.Formatter, MessagesListAdapter.OnMessageViewLongClickListener, + MessagesListAdapter.OnMessageClickListener, ContentChecker, VoiceMessageInterface, CommonMessageInterface, @@ -386,6 +389,8 @@ class ChatActivity : var credentials: String? = null var currentConversation: ConversationModel? = null var adapter: TalkMessagesListAdapter? = null + private var lastMessageClickTime = 0L + private var lastMessageId = "" var mentionAutocomplete: Autocomplete<*>? = null var layoutManager: LinearLayoutManager? = null var pullChatMessagesPending = false @@ -1644,6 +1649,7 @@ class ChatActivity : adapter?.setLoadMoreListener(this) adapter?.setDateHeadersFormatter { format(it) } adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } + adapter?.setOnMessageClickListener{ message -> onMessageClick(message) } adapter?.registerViewClickListener( R.id.playPauseBtn @@ -4087,6 +4093,20 @@ class ChatActivity : openMessageActionsDialog(message) } + override fun onMessageClick(message: IMessage) { + val now = SystemClock.elapsedRealtime() + if (now - lastMessageClickTime < ViewConfiguration.getDoubleTapTimeout() && + message.id?.equals(lastMessageId) == true + ) { + openMessageActionsDialog(message) + lastMessageClickTime = 0L + lastMessageId = "" + } else { + lastMessageClickTime = now + lastMessageId = message.id + } + } + override fun onPreviewMessageLongClick(chatMessage: ChatMessage) { onOpenMessageActionsDialog(chatMessage) } From a0f61e9c56786fb9a26d22dfbc8be7f0ab58b823 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 25 Feb 2026 22:35:30 +0100 Subject: [PATCH 2/2] feat(chat): Double tap on message elements to react, not covered via the chatkit click listeners Resolves #5240 Signed-off-by: Andy Scherzinger --- .../messages/IncomingDeckCardViewHolder.kt | 23 ++++++-- .../IncomingLinkPreviewMessageViewHolder.kt | 57 +++++++++++++------ .../messages/IncomingTextMessageViewHolder.kt | 23 +++++++- .../IncomingVoiceMessageViewHolder.kt | 1 - .../OutcomingLinkPreviewMessageViewHolder.kt | 29 ++++++---- .../OutcomingTextMessageViewHolder.kt | 23 +++++++- .../com/nextcloud/talk/chat/ChatActivity.kt | 2 +- 7 files changed, 118 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt index 5bef49be2f..e2867d8060 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt @@ -11,6 +11,8 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat @@ -74,7 +76,7 @@ class IncomingDeckCardViewHolder(incomingView: View, payload: Any) : var boardName: String? = null var cardLink: String? = null - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") override fun onBind(message: ChatMessage) { super.onBind(message) this.message = message @@ -95,9 +97,22 @@ class IncomingDeckCardViewHolder(incomingView: View, payload: Any) : // parent message handling setParentMessageDataOnMessageItem(message) - binding.cardView.setOnLongClickListener { l: View? -> - commonMessageInterface.onOpenMessageActionsDialog(message) - true + binding.cardView.isLongClickable = true + val cardViewGestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + commonMessageInterface.onOpenMessageActionsDialog(message) + } + override fun onDoubleTap(e: MotionEvent): Boolean { + commonMessageInterface.onOpenMessageActionsDialog(message) + return true + } + } + ) + binding.cardView.setOnTouchListener { _, event -> + cardViewGestureDetector.onTouchEvent(event) + false } binding.cardView.setOnClickListener { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt index 250b34ec5b..12df33bfd6 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt @@ -9,7 +9,10 @@ package com.nextcloud.talk.adapters.messages import android.annotation.SuppressLint import android.content.Context +import android.text.Spanned import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import androidx.core.content.ContextCompat import autodagger.AutoInjector @@ -64,27 +67,14 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : lateinit var commonMessageInterface: CommonMessageInterface - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") override fun onBind(message: ChatMessage) { super.onBind(message) this.message = message sharedApplication!!.componentApplication.inject(this) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) - var processedMessageText = messageUtils.enrichChatMessageText( - binding.messageText.context, - message, - true, - viewThemeUtils - ) - - processedMessageText = messageUtils.processMessageParameters( - binding.messageText.context, - viewThemeUtils, - processedMessageText!!, - message, - itemView - ) + val processedMessageText = processMessageText(message) binding.messageText.text = processedMessageText @@ -103,9 +93,22 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : binding.referenceInclude, itemView.context ) - binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? -> - commonMessageInterface.onOpenMessageActionsDialog(message) - true + binding.referenceInclude.referenceWrapper.isLongClickable = true + val referenceWrapperGestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + commonMessageInterface.onOpenMessageActionsDialog(message) + } + override fun onDoubleTap(e: MotionEvent): Boolean { + commonMessageInterface.onOpenMessageActionsDialog(message) + return true + } + } + ) + binding.referenceInclude.referenceWrapper.setOnTouchListener { _, event -> + referenceWrapperGestureDetector.onTouchEvent(event) + false } itemView.setTag(R.string.replyable_message_view_tag, message.replyable) @@ -134,6 +137,24 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : ) } + private fun processMessageText(message: ChatMessage): Spanned? { + var processedMessageText = messageUtils.enrichChatMessageText( + binding.messageText.context, + message, + true, + viewThemeUtils + ) + + processedMessageText = messageUtils.processMessageParameters( + binding.messageText.context, + viewThemeUtils, + processedMessageText!!, + message, + itemView + ) + return processedMessageText + } + private fun longClickOnReaction(chatMessage: ChatMessage) { commonMessageInterface.onLongClickReactions(chatMessage) } 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 b1722de2d4..f6f015dc64 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 @@ -9,9 +9,12 @@ */ package com.nextcloud.talk.adapters.messages +import android.annotation.SuppressLint import android.content.Context import android.util.Log import android.util.TypedValue +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.CheckBox @@ -98,6 +101,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : processMessage(message, hasCheckboxes) } + @SuppressLint("ClickableViewAccessibility") @Suppress("LongMethod") private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) { var textSize = context.resources!!.getDimension(R.dimen.chat_text_size) @@ -181,9 +185,22 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) : View.GONE } - binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? -> - commonMessageInterface.onOpenMessageActionsDialog(message) - true + binding.messageQuote.quotedChatMessageView.isLongClickable = true + val quotedChatMessageViewGestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + commonMessageInterface.onOpenMessageActionsDialog(message) + } + override fun onDoubleTap(e: MotionEvent): Boolean { + commonMessageInterface.onOpenMessageActionsDialog(message) + return true + } + } + ) + binding.messageQuote.quotedChatMessageView.setOnTouchListener { _, event -> + quotedChatMessageViewGestureDetector.onTouchEvent(event) + false } itemView.setTag(R.string.replyable_message_view_tag, message.replyable) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index 97d01f650c..d737080365 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -95,7 +95,6 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) setAvatarAndAuthorOnMessageItem(message) - colorizeMessageBubble(message) itemView.isSelected = false diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt index fcf78cf6e4..212961a91c 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt @@ -11,6 +11,8 @@ package com.nextcloud.talk.adapters.messages import android.annotation.SuppressLint import android.content.Context import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import androidx.appcompat.content.res.AppCompatResources import autodagger.AutoInjector @@ -67,7 +69,7 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) : lateinit var commonMessageInterface: CommonMessageInterface - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") override fun onBind(message: ChatMessage) { super.onBind(message) this.message = message @@ -87,7 +89,6 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) : ) binding.messageText.text = processedMessageText - itemView.isSelected = false // parent message handling @@ -114,15 +115,23 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) : binding.checkMark.contentDescription = readStatusContentDescriptionString - LinkPreview().showLink( - message, - ncApi, - binding.referenceInclude, - itemView.context + LinkPreview().showLink(message, ncApi, binding.referenceInclude, itemView.context) + binding.referenceInclude.referenceWrapper.isLongClickable = true + val referenceWrapperGestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + commonMessageInterface.onOpenMessageActionsDialog(message) + } + override fun onDoubleTap(e: MotionEvent): Boolean { + commonMessageInterface.onOpenMessageActionsDialog(message) + return true + } + } ) - binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? -> - commonMessageInterface.onOpenMessageActionsDialog(message) - true + binding.referenceInclude.referenceWrapper.setOnTouchListener { _, event -> + referenceWrapperGestureDetector.onTouchEvent(event) + false } itemView.setTag(R.string.replyable_message_view_tag, message.replyable) 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 1f7afdc449..46e352b6d6 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 @@ -9,9 +9,12 @@ */ package com.nextcloud.talk.adapters.messages +import android.annotation.SuppressLint import android.content.Context import android.util.Log import android.util.TypedValue +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.CheckBox @@ -102,6 +105,7 @@ class OutcomingTextMessageViewHolder(itemView: View) : processMessage(message, hasCheckboxes) } + @SuppressLint("ClickableViewAccessibility") @Suppress("Detekt.LongMethod") private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) { var isBubbled = true @@ -196,9 +200,22 @@ class OutcomingTextMessageViewHolder(itemView: View) : View.GONE } - binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? -> - commonMessageInterface.onOpenMessageActionsDialog(message) - true + binding.messageQuote.quotedChatMessageView.isLongClickable = true + val quotedChatMessageViewGestureDetector = GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + commonMessageInterface.onOpenMessageActionsDialog(message) + } + override fun onDoubleTap(e: MotionEvent): Boolean { + commonMessageInterface.onOpenMessageActionsDialog(message) + return true + } + } + ) + binding.messageQuote.quotedChatMessageView.setOnTouchListener { _, event -> + quotedChatMessageViewGestureDetector.onTouchEvent(event) + false } binding.checkMark.visibility = View.INVISIBLE 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 3488623013..884e805fb6 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -1649,7 +1649,7 @@ class ChatActivity : adapter?.setLoadMoreListener(this) adapter?.setDateHeadersFormatter { format(it) } adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } - adapter?.setOnMessageClickListener{ message -> onMessageClick(message) } + adapter?.setOnMessageClickListener { message -> onMessageClick(message) } adapter?.registerViewClickListener( R.id.playPauseBtn