From 063190dce1bc812d9006eabc8edb68cce2c9ca3a Mon Sep 17 00:00:00 2001 From: Tomas Timinskas Date: Wed, 13 Mar 2024 17:00:52 -0300 Subject: [PATCH 1/3] Initial logic for highlighted text --- .../sphinx/chat_common/ui/ChatViewModel.kt | 1 + .../ui/viewstate/messageholder/LayoutState.kt | 1 + .../messageholder/MessageHolderViewState.kt | 2 ++ .../sphinx/dashboard/ui/DashboardViewModel.kt | 33 +++++++++++++++++++ 4 files changed, 37 insertions(+) diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt index d04bc8e933..58dca36f06 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt @@ -929,6 +929,7 @@ abstract class ChatViewModel( text?.let { nnText -> messageLayoutState = LayoutState.Bubble.ContainerThird.Message( text = nnText, + highlightedTexts = emptyList(), decryptionError = false, isThread = false ) diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt index 6bb4d896b7..a8b4c06e78 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt @@ -208,6 +208,7 @@ sealed class LayoutState private constructor() { data class Message( val text: String?, + val highlightedTexts: List, val decryptionError: Boolean, val isThread: Boolean ): ContainerThird() diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt index a968c5219a..70b2490696 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt @@ -228,6 +228,7 @@ internal sealed class MessageHolderViewState( if (text.isNotEmpty()) { LayoutState.Bubble.ContainerThird.Message( text = text, + highlightedTexts = emptyList(), decryptionError = false, isThread = isThread ) @@ -238,6 +239,7 @@ internal sealed class MessageHolderViewState( if (decryptionError) { LayoutState.Bubble.ContainerThird.Message( text = null, + highlightedTexts = emptyList(), decryptionError = true, isThread = isThread ) diff --git a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt index 931908bf83..2a41da37da 100644 --- a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt +++ b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt @@ -4,6 +4,8 @@ import android.app.Application import android.content.Context import android.content.Intent import android.net.Uri +import android.util.Range +import androidx.annotation.IntRange import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import app.cash.exhaustive.Exhaustive @@ -79,6 +81,7 @@ import kotlinx.coroutines.flow.* import org.jitsi.meet.sdk.JitsiMeetActivity import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetUserInfo +import java.util.Locale import javax.inject.Inject @@ -194,6 +197,8 @@ internal class DashboardViewModel @Inject constructor( } fun toggleHideBalanceState(){ + val results = "`test` hello and now? `test and now` and now `hello\n and now`".highlightedTexts() + viewModelScope.launch(mainImmediate) { val newState = if(_hideBalanceStateFlow.value == HideBalance.DISABLED){ HideBalance.ENABLED @@ -1330,3 +1335,31 @@ internal class DashboardViewModel @Inject constructor( } } +@Suppress("NOTHING_TO_INLINE") +inline fun String.highlightedTexts(): List> { + val matcher = "`([^`]*)`".toRegex() + val ranges = matcher.findAll(this).map{ it.range }.toList() + + val adaptedText = this.replace("`", "") + var matches: MutableList> = mutableListOf() + + ranges.forEachIndexed { index, range -> + val subtraction = index * 2 + + val adaptedRange = IntRange( + from = (range.first - subtraction).toLong(), + to = (range.last - subtraction - 1).toLong() + ) + + val rangeString = adaptedText.substring(adaptedRange.from.toInt(), adaptedRange.to.toInt()) + + matches.add( + Pair(rangeString, adaptedRange) + ) + } + + println(matches) + + return matches +} + From fbebfe99712ec3f4c45714787dae6aefcd1b42f8 Mon Sep 17 00:00:00 2001 From: Tomas Timinskas Date: Thu, 14 Mar 2024 17:31:01 -0300 Subject: [PATCH 2/3] Logic implemented to highlight text on messages (thread list and header pending) --- .../resources/src/main/res/values/colors.xml | 2 + .../sphinx/chat_common/ui/ChatViewModel.kt | 8 +- .../messageholder/HolderBindingExtensions.kt | 19 +- .../ui/viewstate/messageholder/LayoutState.kt | 3 +- .../messageholder/MessageHolderViewState.kt | 6 +- .../util/SphinxHighlightingTool.kt | 178 ++++++++++++++++++ .../sphinx/dashboard/ui/DashboardViewModel.kt | 103 +++++----- 7 files changed, 269 insertions(+), 50 deletions(-) create mode 100644 sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt diff --git a/sphinx/application/common/resources/src/main/res/values/colors.xml b/sphinx/application/common/resources/src/main/res/values/colors.xml index 0984cbde07..0b56ba63b9 100644 --- a/sphinx/application/common/resources/src/main/res/values/colors.xml +++ b/sphinx/application/common/resources/src/main/res/values/colors.xml @@ -105,6 +105,8 @@ #FAE676 #222E3A + #26FFFFFF + #7077FF #DBD23C diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt index 58dca36f06..8b913e5fa8 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt @@ -16,6 +16,7 @@ import android.provider.OpenableColumns import android.util.Log import android.webkit.MimeTypeMap import androidx.annotation.CallSuper +import androidx.annotation.IntRange import androidx.core.view.inputmethod.InputConnectionCompat import androidx.fragment.app.FragmentManager import androidx.lifecycle.SavedStateHandle @@ -46,6 +47,8 @@ import chat.sphinx.chat_common.util.AudioPlayerController import chat.sphinx.chat_common.util.AudioPlayerControllerImpl import chat.sphinx.chat_common.util.AudioRecorderController import chat.sphinx.chat_common.util.SphinxLinkify +import chat.sphinx.chat_common.util.highlightedTexts +import chat.sphinx.chat_common.util.replacingHighlightedDelimiters import chat.sphinx.concept_image_loader.ImageLoaderOptions import chat.sphinx.concept_link_preview.LinkPreviewHandler import chat.sphinx.concept_link_preview.model.TribePreviewName @@ -567,7 +570,6 @@ abstract class ChatViewModel( newList.add( MessageHolderViewState.Sent( message, - chat, tribeAdmin, background = when { @@ -928,8 +930,8 @@ abstract class ChatViewModel( text?.let { nnText -> messageLayoutState = LayoutState.Bubble.ContainerThird.Message( - text = nnText, - highlightedTexts = emptyList(), + text = nnText.replacingHighlightedDelimiters(), + highlightedTexts = nnText.highlightedTexts(), decryptionError = false, isThread = false ) diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt index 3ef27119a0..263aeb8ccb 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt @@ -1,6 +1,7 @@ package chat.sphinx.chat_common.ui.viewstate.messageholder import android.graphics.Color +import android.os.Build import android.view.Gravity import android.view.View import android.webkit.WebView @@ -10,6 +11,7 @@ import android.widget.ImageView import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.MainThread +import androidx.annotation.RequiresApi import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat @@ -27,6 +29,7 @@ import chat.sphinx.chat_common.model.UnspecifiedUrl import chat.sphinx.chat_common.ui.viewstate.audio.AudioMessageState import chat.sphinx.chat_common.ui.viewstate.audio.AudioPlayState import chat.sphinx.chat_common.util.AudioPlayerController +import chat.sphinx.chat_common.util.SphinxHighlightingTool import chat.sphinx.chat_common.util.SphinxLinkify import chat.sphinx.chat_common.util.SphinxUrlSpan import chat.sphinx.chat_common.util.VideoThumbnailUtil @@ -244,7 +247,7 @@ internal fun LayoutMessageHolderBinding.setView( lifecycleScope, userColorsHelper, audioPlayerController, - loadImage = { imageView, url, -> + loadImage = { imageView, url -> lifecycleScope.launch(dispatchers.mainImmediate) { imageLoader.load( imageView, @@ -1128,6 +1131,7 @@ internal inline fun LayoutMessageHolderBinding.setBubbleThreadLayout( } +@RequiresApi(Build.VERSION_CODES.P) @MainThread @Suppress("NOTHING_TO_INLINE") internal inline fun LayoutMessageHolderBinding.setBubbleMessageLayout( @@ -1150,8 +1154,19 @@ internal inline fun LayoutMessageHolderBinding.setBubbleMessageLayout( ) setTextColor(textColor) + SphinxHighlightingTool.addHighlights( + this, + message.highlightedTexts, + resources, + context + ) + if (onSphinxInteractionListener != null) { - SphinxLinkify.addLinks(this, SphinxLinkify.ALL, includeMessageHolderBubble.root.context, onSphinxInteractionListener) + SphinxLinkify.addLinks( + this, + SphinxLinkify.ALL, includeMessageHolderBubble.root.context, + onSphinxInteractionListener + ) } } } diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt index a8b4c06e78..386932caba 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/LayoutState.kt @@ -1,5 +1,6 @@ package chat.sphinx.chat_common.ui.viewstate.messageholder +import androidx.annotation.IntRange import chat.sphinx.concept_link_preview.model.* import chat.sphinx.wrapper_chat.ChatType import chat.sphinx.wrapper_common.FileSize @@ -208,7 +209,7 @@ sealed class LayoutState private constructor() { data class Message( val text: String?, - val highlightedTexts: List, + val highlightedTexts: List>, val decryptionError: Boolean, val isThread: Boolean ): ContainerThird() diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt index 70b2490696..17f4ecdf3b 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt @@ -8,6 +8,8 @@ import chat.sphinx.chat_common.model.MessageLinkPreview import chat.sphinx.chat_common.ui.viewstate.InitialHolderViewState import chat.sphinx.chat_common.ui.viewstate.selected.MenuItemState import chat.sphinx.chat_common.util.SphinxLinkify +import chat.sphinx.chat_common.util.highlightedTexts +import chat.sphinx.chat_common.util.replacingHighlightedDelimiters import chat.sphinx.wrapper_chat.Chat import chat.sphinx.wrapper_chat.isConversation import chat.sphinx.wrapper_chat.isTribe @@ -227,8 +229,8 @@ internal sealed class MessageHolderViewState( message.retrieveTextToShow()?.let { text -> if (text.isNotEmpty()) { LayoutState.Bubble.ContainerThird.Message( - text = text, - highlightedTexts = emptyList(), + text = text.replacingHighlightedDelimiters(), + highlightedTexts = text.highlightedTexts(), decryptionError = false, isThread = isThread ) diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt new file mode 100644 index 0000000000..bd380393b9 --- /dev/null +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt @@ -0,0 +1,178 @@ +package chat.sphinx.chat_common.util + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.os.Build +import android.text.Spannable +import android.text.SpannableString +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.text.style.BackgroundColorSpan +import android.text.style.TypefaceSpan +import android.text.style.URLSpan +import android.text.util.Linkify +import android.text.util.Linkify.MatchFilter +import android.text.util.Linkify.TransformFilter +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.IntDef +import androidx.annotation.IntRange +import androidx.annotation.RequiresApi +import androidx.annotation.RestrictTo +import androidx.core.content.res.ResourcesCompat +import androidx.core.util.PatternsCompat +import chat.sphinx.chat_common.R +import chat.sphinx.wrapper_common.feed.FeedItemLink +import chat.sphinx.wrapper_common.lightning.LightningNodePubKey +import chat.sphinx.wrapper_common.lightning.VirtualLightningNodeAddress +import chat.sphinx.wrapper_common.tribe.TribeJoinLink +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern + + +/** + * LinkifyCompat brings in `Linkify` improvements for URLs and email addresses to older API + * levels. + */ +@SuppressLint("RestrictedApi") +object SphinxHighlightingTool { + /** + * Scans the text of the provided TextView and turns all occurrences of + * the link types indicated in the mask into clickable links. If matches + * are found the movement method for the TextView is set to + * LinkMovementMethod. + * + * @param text TextView whose text is to be marked-up with links + * @param mask Mask to define which kinds of links will be searched. + * + * @return True if at least one link is found and applied. + */ + @RequiresApi(Build.VERSION_CODES.P) + fun addHighlights( + text: TextView, + highlightedTexts: List>, + resources: Resources, + context: Context + ) { + + if (highlightedTexts.isNotEmpty()) { + val t = text.text + if (t is Spannable) { + for (highlightedText in highlightedTexts) { + ResourcesCompat.getFont(context, R.font.roboto_light)?.let { typeface -> + t.setSpan( + TypefaceSpan(typeface), + highlightedText.second.from.toInt(), + highlightedText.second.to.toInt(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + t.setSpan( + BackgroundColorSpan(resources.getColor(R.color.highlightedTextBackground)), + highlightedText.second.from.toInt(), + highlightedText.second.to.toInt(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + text.setText(t, TextView.BufferType.SPANNABLE) + } + } else { + val spannable: Spannable = SpannableString(text.text) + + for (highlightedText in highlightedTexts) { + ResourcesCompat.getFont(context, R.font.roboto_light)?.let { typeface -> + spannable.setSpan( + TypefaceSpan(typeface), + highlightedText.second.from.toInt(), + highlightedText.second.to.toInt(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + spannable.setSpan( + BackgroundColorSpan(resources.getColor(R.color.highlightedTextBackground)), + highlightedText.second.from.toInt(), + highlightedText.second.to.toInt(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + text.setText(spannable, TextView.BufferType.SPANNABLE) + } + } + } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun String.highlightedTexts(): List> { + val matcher = "`([^`]*)`".toRegex() + val ranges = matcher.findAll(this).map{ it.range }.toList() + + if (ranges.isEmpty()) { + return emptyList() + } + + var adaptedText = this + + ranges.forEachIndexed { index, range -> + val subtraction = index * 2 + + val adaptedRange = IntRange( + start = range.first - subtraction, + endInclusive = range.last - subtraction + ) + + val rangeString = adaptedText.substring(adaptedRange.first, adaptedRange.last).replace("`","") + + adaptedText = adaptedText.replaceRange(adaptedRange, rangeString) + } + + var matches: MutableList> = mutableListOf() + + ranges.forEachIndexed { index, range -> + val subtraction = index * 2 + + val adaptedRange = IntRange( + from = (range.first - subtraction).toLong(), + to = (range.last - subtraction - 1).toLong() + ) + + val rangeString = adaptedText.substring(adaptedRange.from.toInt(), adaptedRange.to.toInt()) + + matches.add( + Pair(rangeString, adaptedRange) + ) + } + + return matches +} + +@Suppress("NOTHING_TO_INLINE") +inline fun String.replacingHighlightedDelimiters(): String { + val matcher = "`([^`]*)`".toRegex() + val ranges = matcher.findAll(this).map{ it.range }.toList() + + if (ranges.isEmpty()) { + return this + } + + var adaptedText = this + + ranges.forEachIndexed { index, range -> + val subtraction = index * 2 + + val adaptedRange = IntRange( + start = range.first - subtraction, + endInclusive = range.last - subtraction + ) + + val rangeString = adaptedText.substring(adaptedRange.first, adaptedRange.last).replace("`","") + + adaptedText = adaptedText.replaceRange(adaptedRange, rangeString) + } + + return adaptedText +} \ No newline at end of file diff --git a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt index 2a41da37da..27a71c57de 100644 --- a/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt +++ b/sphinx/screens/dashboard/dashboard/src/main/java/chat/sphinx/dashboard/ui/DashboardViewModel.kt @@ -4,8 +4,6 @@ import android.app.Application import android.content.Context import android.content.Intent import android.net.Uri -import android.util.Range -import androidx.annotation.IntRange import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import app.cash.exhaustive.Exhaustive @@ -39,8 +37,14 @@ import chat.sphinx.dashboard.R import chat.sphinx.dashboard.navigation.DashboardBottomNavBarNavigator import chat.sphinx.dashboard.navigation.DashboardNavDrawerNavigator import chat.sphinx.dashboard.navigation.DashboardNavigator -import chat.sphinx.dashboard.ui.viewstates.* -import chat.sphinx.kotlin_response.* +import chat.sphinx.dashboard.ui.viewstates.ChatListFooterButtonsViewState +import chat.sphinx.dashboard.ui.viewstates.DashboardMotionViewState +import chat.sphinx.dashboard.ui.viewstates.DashboardTabsViewState +import chat.sphinx.dashboard.ui.viewstates.DeepLinkPopupViewState +import chat.sphinx.kotlin_response.LoadResponse +import chat.sphinx.kotlin_response.Response +import chat.sphinx.kotlin_response.ResponseError +import chat.sphinx.kotlin_response.exception import chat.sphinx.logger.SphinxLogger import chat.sphinx.menu_bottom.ui.MenuBottomViewState import chat.sphinx.menu_bottom_scanner.ScannerMenuHandler @@ -50,22 +54,62 @@ import chat.sphinx.scanner_view_model_coordinator.request.ScannerRequest import chat.sphinx.scanner_view_model_coordinator.response.ScannerResponse import chat.sphinx.wrapper_chat.Chat import chat.sphinx.wrapper_chat.isConversation -import chat.sphinx.wrapper_common.* +import chat.sphinx.wrapper_common.CreateInvoiceLink +import chat.sphinx.wrapper_common.ExternalAuthorizeLink +import chat.sphinx.wrapper_common.ExternalRequestLink +import chat.sphinx.wrapper_common.FeedRecommendationsToggle +import chat.sphinx.wrapper_common.HideBalance +import chat.sphinx.wrapper_common.PeopleConnectLink +import chat.sphinx.wrapper_common.RedeemSatsLink +import chat.sphinx.wrapper_common.StakworkAuthorizeLink import chat.sphinx.wrapper_common.chat.ChatUUID import chat.sphinx.wrapper_common.chat.PushNotificationLink import chat.sphinx.wrapper_common.chat.toPushNotificationLink import chat.sphinx.wrapper_common.dashboard.ChatId import chat.sphinx.wrapper_common.dashboard.RestoreProgressViewState import chat.sphinx.wrapper_common.dashboard.toChatId -import chat.sphinx.wrapper_common.feed.* -import chat.sphinx.wrapper_common.lightning.* +import chat.sphinx.wrapper_common.feed.FeedItemLink +import chat.sphinx.wrapper_common.feed.toFeedItemLink +import chat.sphinx.wrapper_common.isValidExternalAuthorizeLink +import chat.sphinx.wrapper_common.isValidExternalRequestLink +import chat.sphinx.wrapper_common.isValidPeopleConnectLink +import chat.sphinx.wrapper_common.lightning.Bolt11 +import chat.sphinx.wrapper_common.lightning.LightningNodePubKey +import chat.sphinx.wrapper_common.lightning.LightningPaymentRequest +import chat.sphinx.wrapper_common.lightning.LightningRouteHint +import chat.sphinx.wrapper_common.lightning.Sat +import chat.sphinx.wrapper_common.lightning.getPubKey +import chat.sphinx.wrapper_common.lightning.getRouteHint +import chat.sphinx.wrapper_common.lightning.isValidLightningNodeLink +import chat.sphinx.wrapper_common.lightning.isValidLightningNodePubKey +import chat.sphinx.wrapper_common.lightning.isValidLightningPaymentRequest +import chat.sphinx.wrapper_common.lightning.isValidVirtualNodeAddress +import chat.sphinx.wrapper_common.lightning.toLightningNodePubKey +import chat.sphinx.wrapper_common.lightning.toLightningPaymentRequestOrNull +import chat.sphinx.wrapper_common.lightning.toLightningRouteHint +import chat.sphinx.wrapper_common.lightning.toSat +import chat.sphinx.wrapper_common.lightning.toVirtualLightningNodeAddress import chat.sphinx.wrapper_common.message.SphinxCallLink import chat.sphinx.wrapper_common.message.toSphinxCallLink +import chat.sphinx.wrapper_common.toCreateInvoiceLink +import chat.sphinx.wrapper_common.toExternalAuthorizeLink +import chat.sphinx.wrapper_common.toExternalRequestLink +import chat.sphinx.wrapper_common.toPeopleConnectLink +import chat.sphinx.wrapper_common.toPhotoUrl +import chat.sphinx.wrapper_common.toRedeemSatsLink +import chat.sphinx.wrapper_common.toStakworkAuthorizeLink import chat.sphinx.wrapper_common.tribe.TribeJoinLink import chat.sphinx.wrapper_common.tribe.isValidTribeJoinLink import chat.sphinx.wrapper_common.tribe.toTribeJoinLink -import chat.sphinx.wrapper_contact.* -import chat.sphinx.wrapper_feed.* +import chat.sphinx.wrapper_contact.Contact +import chat.sphinx.wrapper_contact.ContactAlias +import chat.sphinx.wrapper_contact.avatarUrl +import chat.sphinx.wrapper_contact.toContactAlias +import chat.sphinx.wrapper_contact.toContactKey +import chat.sphinx.wrapper_feed.Feed +import chat.sphinx.wrapper_feed.isNewsletter +import chat.sphinx.wrapper_feed.isPodcast +import chat.sphinx.wrapper_feed.isVideo import chat.sphinx.wrapper_lightning.NodeBalance import chat.sphinx.wrapper_relay.RelayUrl import com.squareup.moshi.Moshi @@ -76,12 +120,17 @@ import io.matthewnelson.android_feature_viewmodel.submitSideEffect import io.matthewnelson.build_config.BuildConfigVersionCode import io.matthewnelson.concept_coroutines.CoroutineDispatchers import io.matthewnelson.concept_views.viewstate.ViewStateContainer -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jitsi.meet.sdk.JitsiMeetActivity import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetUserInfo -import java.util.Locale import javax.inject.Inject @@ -197,8 +246,6 @@ internal class DashboardViewModel @Inject constructor( } fun toggleHideBalanceState(){ - val results = "`test` hello and now? `test and now` and now `hello\n and now`".highlightedTexts() - viewModelScope.launch(mainImmediate) { val newState = if(_hideBalanceStateFlow.value == HideBalance.DISABLED){ HideBalance.ENABLED @@ -1335,31 +1382,3 @@ internal class DashboardViewModel @Inject constructor( } } -@Suppress("NOTHING_TO_INLINE") -inline fun String.highlightedTexts(): List> { - val matcher = "`([^`]*)`".toRegex() - val ranges = matcher.findAll(this).map{ it.range }.toList() - - val adaptedText = this.replace("`", "") - var matches: MutableList> = mutableListOf() - - ranges.forEachIndexed { index, range -> - val subtraction = index * 2 - - val adaptedRange = IntRange( - from = (range.first - subtraction).toLong(), - to = (range.last - subtraction - 1).toLong() - ) - - val rangeString = adaptedText.substring(adaptedRange.from.toInt(), adaptedRange.to.toInt()) - - matches.add( - Pair(rangeString, adaptedRange) - ) - } - - println(matches) - - return matches -} - From ba5eecde05e5eb23694ca3ccd5d108735f5503ec Mon Sep 17 00:00:00 2001 From: Tomas Timinskas Date: Fri, 15 Mar 2024 12:15:58 -0300 Subject: [PATCH 3/3] - Last changes for highlighting text - Fix for grouping messages logic on thread view - Fix for threads list showing thread with 1 reply --- settings.gradle | 1 + .../common/highlighting-tool/.gitignore | 1 + .../common/highlighting-tool/build.gradle | 35 ++++++++++++++ .../highlighting-tool/consumer-rules.pro | 0 .../highlighting-tool/proguard-rules.pro | 21 ++++++++ .../src/main/AndroidManifest.xml | 4 ++ .../SphinxHighlightingTool.kt | 48 +++++++------------ .../chat-common/chat-common/build.gradle | 2 +- .../adapters/MessageListAdapter.kt | 10 ++++ .../sphinx/chat_common/ui/ChatViewModel.kt | 12 ++--- .../messageholder/HolderBindingExtensions.kt | 3 +- .../messageholder/MessageHolderViewState.kt | 5 +- .../viewstate/thread/ThreadHeaderViewState.kt | 2 + sphinx/screens/threads/threads/build.gradle | 1 + .../sphinx/threads/adapter/ThreadsAdapter.kt | 18 +++++-- .../chat/sphinx/threads/model/ThreadItem.kt | 2 + .../sphinx/threads/ui/ThreadsViewModel.kt | 10 +++- 17 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 sphinx/application/common/highlighting-tool/.gitignore create mode 100644 sphinx/application/common/highlighting-tool/build.gradle create mode 100644 sphinx/application/common/highlighting-tool/consumer-rules.pro create mode 100644 sphinx/application/common/highlighting-tool/proguard-rules.pro create mode 100644 sphinx/application/common/highlighting-tool/src/main/AndroidManifest.xml rename sphinx/{screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util => application/common/highlighting-tool/src/main/java/chat/sphinx/highlighting_tool}/SphinxHighlightingTool.kt (76%) diff --git a/settings.gradle b/settings.gradle index 8d6e547b3d..631e364596 100644 --- a/settings.gradle +++ b/settings.gradle @@ -348,3 +348,4 @@ include ':sphinx:activity:concepts:concept-signer-manager' include ':sphinx:activity:features:signer-manager' include ':sphinx:application:common:menus:menu-bottom-signer' include ':sphinx:application:common:menus:menu-bottom-phone-signer-method' +include ':sphinx:application:common:highlighting-tool' diff --git a/sphinx/application/common/highlighting-tool/.gitignore b/sphinx/application/common/highlighting-tool/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/application/common/highlighting-tool/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/application/common/highlighting-tool/build.gradle b/sphinx/application/common/highlighting-tool/build.gradle new file mode 100644 index 0000000000..c54df5828a --- /dev/null +++ b/sphinx/application/common/highlighting-tool/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + buildFeatures.viewBinding = true + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments disableAnalytics: 'true' + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + } + } + namespace 'chat.sphinx.highlighting_tool' +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + + implementation project(path: ':sphinx:application:common:resources') + + implementation deps.androidx.appCompat +} \ No newline at end of file diff --git a/sphinx/application/common/highlighting-tool/consumer-rules.pro b/sphinx/application/common/highlighting-tool/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sphinx/application/common/highlighting-tool/proguard-rules.pro b/sphinx/application/common/highlighting-tool/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/sphinx/application/common/highlighting-tool/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sphinx/application/common/highlighting-tool/src/main/AndroidManifest.xml b/sphinx/application/common/highlighting-tool/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a5918e68ab --- /dev/null +++ b/sphinx/application/common/highlighting-tool/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt b/sphinx/application/common/highlighting-tool/src/main/java/chat/sphinx/highlighting_tool/SphinxHighlightingTool.kt similarity index 76% rename from sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt rename to sphinx/application/common/highlighting-tool/src/main/java/chat/sphinx/highlighting_tool/SphinxHighlightingTool.kt index bd380393b9..4efdf98a8a 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/util/SphinxHighlightingTool.kt +++ b/sphinx/application/common/highlighting-tool/src/main/java/chat/sphinx/highlighting_tool/SphinxHighlightingTool.kt @@ -1,4 +1,4 @@ -package chat.sphinx.chat_common.util +package chat.sphinx.highlighting_tool import android.annotation.SuppressLint import android.content.Context @@ -6,30 +6,13 @@ import android.content.res.Resources import android.os.Build import android.text.Spannable import android.text.SpannableString -import android.text.Spanned -import android.text.method.LinkMovementMethod import android.text.style.BackgroundColorSpan import android.text.style.TypefaceSpan -import android.text.style.URLSpan -import android.text.util.Linkify -import android.text.util.Linkify.MatchFilter -import android.text.util.Linkify.TransformFilter import android.widget.TextView -import androidx.annotation.ColorInt -import androidx.annotation.IntDef import androidx.annotation.IntRange import androidx.annotation.RequiresApi -import androidx.annotation.RestrictTo import androidx.core.content.res.ResourcesCompat -import androidx.core.util.PatternsCompat -import chat.sphinx.chat_common.R -import chat.sphinx.wrapper_common.feed.FeedItemLink -import chat.sphinx.wrapper_common.lightning.LightningNodePubKey -import chat.sphinx.wrapper_common.lightning.VirtualLightningNodeAddress -import chat.sphinx.wrapper_common.tribe.TribeJoinLink import java.util.* -import java.util.regex.Matcher -import java.util.regex.Pattern /** @@ -49,7 +32,6 @@ object SphinxHighlightingTool { * * @return True if at least one link is found and applied. */ - @RequiresApi(Build.VERSION_CODES.P) fun addHighlights( text: TextView, highlightedTexts: List>, @@ -62,12 +44,14 @@ object SphinxHighlightingTool { if (t is Spannable) { for (highlightedText in highlightedTexts) { ResourcesCompat.getFont(context, R.font.roboto_light)?.let { typeface -> - t.setSpan( - TypefaceSpan(typeface), - highlightedText.second.from.toInt(), - highlightedText.second.to.toInt(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + t.setSpan( + TypefaceSpan(typeface), + highlightedText.second.from.toInt(), + highlightedText.second.to.toInt(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } } t.setSpan( @@ -84,12 +68,14 @@ object SphinxHighlightingTool { for (highlightedText in highlightedTexts) { ResourcesCompat.getFont(context, R.font.roboto_light)?.let { typeface -> - spannable.setSpan( - TypefaceSpan(typeface), - highlightedText.second.from.toInt(), - highlightedText.second.to.toInt(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + spannable.setSpan( + TypefaceSpan(typeface), + highlightedText.second.from.toInt(), + highlightedText.second.to.toInt(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } } spannable.setSpan( diff --git a/sphinx/screens/chats/chat-common/chat-common/build.gradle b/sphinx/screens/chats/chat-common/chat-common/build.gradle index 7db81e3e62..33b6a5998a 100644 --- a/sphinx/screens/chats/chat-common/chat-common/build.gradle +++ b/sphinx/screens/chats/chat-common/chat-common/build.gradle @@ -48,8 +48,8 @@ dependencies { api project(path: ':sphinx:application:common:logger') api project(path: ':sphinx:application:common:resources') api project(path: ':sphinx:application:common:keyboard-inset-fragment') - api project(path: ':sphinx:application:common:wrappers:wrapper-common') + implementation project(path: ':sphinx:application:common:highlighting-tool') api project(path: ':sphinx:application:data:concepts:concept-image-loader') api project(path: ':sphinx:application:data:concepts:concept-media-cache') diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/adapters/MessageListAdapter.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/adapters/MessageListAdapter.kt index 346ac28422..c63a23e9d0 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/adapters/MessageListAdapter.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/adapters/MessageListAdapter.kt @@ -28,6 +28,7 @@ import chat.sphinx.chat_common.ui.viewstate.selected.SelectedMessageViewState import chat.sphinx.chat_common.util.* import chat.sphinx.concept_image_loader.* import chat.sphinx.concept_user_colors_helper.UserColorsHelper +import chat.sphinx.highlighting_tool.SphinxHighlightingTool import chat.sphinx.resources.getRandomHexCode import chat.sphinx.resources.getString import chat.sphinx.resources.setBackgroundRandomColor @@ -40,6 +41,7 @@ import chat.sphinx.wrapper_contact.ContactAlias import chat.sphinx.wrapper_message.Message import chat.sphinx.wrapper_message.MessageType import chat.sphinx.wrapper_view.Px +import com.giphy.sdk.analytics.GiphyPingbacks.context import io.matthewnelson.android_feature_screens.util.gone import io.matthewnelson.android_feature_screens.util.goneIfFalse import io.matthewnelson.android_feature_screens.util.visible @@ -775,9 +777,17 @@ internal class MessageListAdapter( textViewContactMessageHeaderName.text = senderInfo?.second?.value ?: "" textViewThreadDate.text = threadHeader.timestamp + textViewThreadMessageContent.text = threadHeader.bubbleMessage?.text ?: "" textViewThreadMessageContent.goneIfFalse(threadHeader.bubbleMessage?.text?.isNotEmpty() == true) + SphinxHighlightingTool.addHighlights( + textViewThreadMessageContent, + threadHeader.bubbleMessage?.highlightedTexts ?: emptyList(), + textViewThreadMessageContent.resources, + textViewThreadMessageContent.context + ) + textViewThreadDate.post(Runnable { val linesCount: Int = textViewThreadDate.lineCount diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt index 8b913e5fa8..a3843943d4 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/ChatViewModel.kt @@ -16,7 +16,6 @@ import android.provider.OpenableColumns import android.util.Log import android.webkit.MimeTypeMap import androidx.annotation.CallSuper -import androidx.annotation.IntRange import androidx.core.view.inputmethod.InputConnectionCompat import androidx.fragment.app.FragmentManager import androidx.lifecycle.SavedStateHandle @@ -47,8 +46,8 @@ import chat.sphinx.chat_common.util.AudioPlayerController import chat.sphinx.chat_common.util.AudioPlayerControllerImpl import chat.sphinx.chat_common.util.AudioRecorderController import chat.sphinx.chat_common.util.SphinxLinkify -import chat.sphinx.chat_common.util.highlightedTexts -import chat.sphinx.chat_common.util.replacingHighlightedDelimiters +import chat.sphinx.highlighting_tool.highlightedTexts +import chat.sphinx.highlighting_tool.replacingHighlightedDelimiters import chat.sphinx.concept_image_loader.ImageLoaderOptions import chat.sphinx.concept_link_preview.LinkPreviewHandler import chat.sphinx.concept_link_preview.model.TribePreviewName @@ -384,6 +383,8 @@ abstract class ChatViewModel( val groupingMinutesLimit = 5.0 var date = groupingDate ?: message.date + val isPreviousMessageThreadHeader = (previousMessage?.uuid?.value == getThreadUUID()?.value && previousMessage?.type?.isGroupAction() == false) + val shouldAvoidGroupingWithPrevious = (previousMessage?.shouldAvoidGrouping() ?: true) || message.shouldAvoidGrouping() val isGroupedBySenderWithPrevious = @@ -392,7 +393,7 @@ abstract class ChatViewModel( message.date.getMinutesDifferenceWithDateTime(date) < groupingMinutesLimit val groupedWithPrevious = - (!shouldAvoidGroupingWithPrevious && isGroupedBySenderWithPrevious && isGroupedByDateWithPrevious) + (!shouldAvoidGroupingWithPrevious && isGroupedBySenderWithPrevious && isGroupedByDateWithPrevious && !isPreviousMessageThreadHeader) date = if (groupedWithPrevious) date else message.date @@ -479,8 +480,6 @@ abstract class ChatViewModel( openReceivedPaidInvoicesCount > 0 ) - val isOwner: Boolean = message.sender == owner.id - val isThreadHeaderMessage = (message.uuid?.value == getThreadUUID()?.value && index == 0 && !message.type.isGroupAction()) if (isThreadHeaderMessage) { @@ -1508,6 +1507,7 @@ abstract class ChatViewModel( viewState.messageSenderInfo(viewState.message!!), viewState.timestamp, viewState.bubbleMessage?.text, + viewState.bubbleMessage?.highlightedTexts, viewState.bubbleImageAttachment, viewState.bubbleVideoAttachment, viewState.bubbleFileAttachment, diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt index 263aeb8ccb..a2e524f8f7 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/HolderBindingExtensions.kt @@ -29,7 +29,7 @@ import chat.sphinx.chat_common.model.UnspecifiedUrl import chat.sphinx.chat_common.ui.viewstate.audio.AudioMessageState import chat.sphinx.chat_common.ui.viewstate.audio.AudioPlayState import chat.sphinx.chat_common.util.AudioPlayerController -import chat.sphinx.chat_common.util.SphinxHighlightingTool +import chat.sphinx.highlighting_tool.SphinxHighlightingTool import chat.sphinx.chat_common.util.SphinxLinkify import chat.sphinx.chat_common.util.SphinxUrlSpan import chat.sphinx.chat_common.util.VideoThumbnailUtil @@ -1131,7 +1131,6 @@ internal inline fun LayoutMessageHolderBinding.setBubbleThreadLayout( } -@RequiresApi(Build.VERSION_CODES.P) @MainThread @Suppress("NOTHING_TO_INLINE") internal inline fun LayoutMessageHolderBinding.setBubbleMessageLayout( diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt index 17f4ecdf3b..7df69eb745 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/messageholder/MessageHolderViewState.kt @@ -3,13 +3,12 @@ package chat.sphinx.chat_common.ui.viewstate.messageholder import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor.MODE_READ_ONLY -import android.util.Log import chat.sphinx.chat_common.model.MessageLinkPreview import chat.sphinx.chat_common.ui.viewstate.InitialHolderViewState import chat.sphinx.chat_common.ui.viewstate.selected.MenuItemState import chat.sphinx.chat_common.util.SphinxLinkify -import chat.sphinx.chat_common.util.highlightedTexts -import chat.sphinx.chat_common.util.replacingHighlightedDelimiters +import chat.sphinx.highlighting_tool.highlightedTexts +import chat.sphinx.highlighting_tool.replacingHighlightedDelimiters import chat.sphinx.wrapper_chat.Chat import chat.sphinx.wrapper_chat.isConversation import chat.sphinx.wrapper_chat.isTribe diff --git a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/thread/ThreadHeaderViewState.kt b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/thread/ThreadHeaderViewState.kt index 40831dafda..4cffde157b 100644 --- a/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/thread/ThreadHeaderViewState.kt +++ b/sphinx/screens/chats/chat-common/chat-common/src/main/java/chat/sphinx/chat_common/ui/viewstate/thread/ThreadHeaderViewState.kt @@ -1,5 +1,6 @@ package chat.sphinx.chat_common.ui.viewstate.thread +import androidx.annotation.IntRange import chat.sphinx.chat_common.ui.viewstate.messageholder.LayoutState import chat.sphinx.chat_common.ui.viewstate.messageholder.MessageHolderViewState import chat.sphinx.wrapper_common.PhotoUrl @@ -16,6 +17,7 @@ sealed class ThreadHeaderViewState: ViewState() { val senderInfo: Triple?, val date: String, val message: String?, + val highlightedTexts: List>? = emptyList(), val imageAttachment: LayoutState.Bubble.ContainerSecond.ImageAttachment? = null, val videoAttachment: LayoutState.Bubble.ContainerSecond.VideoAttachment? = null, val fileAttachment: LayoutState.Bubble.ContainerSecond.FileAttachment? = null, diff --git a/sphinx/screens/threads/threads/build.gradle b/sphinx/screens/threads/threads/build.gradle index a746747151..41ce64fe0b 100644 --- a/sphinx/screens/threads/threads/build.gradle +++ b/sphinx/screens/threads/threads/build.gradle @@ -34,6 +34,7 @@ dependencies { // KotlinAndroid implementation project(path: ':android:features:android-feature-screens') implementation project(path: ':sphinx:application:common:screen-detail-fragment') + implementation project(path: ':sphinx:application:common:highlighting-tool') // Sphinx implementation project(path: ':sphinx:activity:insetter-activity') diff --git a/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/adapter/ThreadsAdapter.kt b/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/adapter/ThreadsAdapter.kt index bc8ac40150..08bbd11883 100644 --- a/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/adapter/ThreadsAdapter.kt +++ b/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/adapter/ThreadsAdapter.kt @@ -1,10 +1,12 @@ package chat.sphinx.threads.adapter import android.graphics.Color +import android.os.Build import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView +import androidx.annotation.RequiresApi import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -18,6 +20,7 @@ import chat.sphinx.concept_image_loader.ImageLoaderOptions import chat.sphinx.concept_image_loader.OnImageLoadListener import chat.sphinx.concept_image_loader.Transformation import chat.sphinx.concept_user_colors_helper.UserColorsHelper +import chat.sphinx.highlighting_tool.SphinxHighlightingTool import chat.sphinx.resources.databinding.LayoutChatImageSmallInitialHolderBinding import chat.sphinx.resources.getRandomHexCode import chat.sphinx.resources.getString @@ -178,11 +181,11 @@ internal class ThreadsAdapter( init { binding.root.setOnClickListener { - item?.let { threadItem -> - viewModel.navigateToThreadDetail(threadItem.uuid) - } + item?.let { threadItem -> + viewModel.navigateToThreadDetail(threadItem.uuid) } } + } fun bind(position: Int) { binding.apply { @@ -196,12 +199,19 @@ internal class ThreadsAdapter( // General Info textViewContactHeaderName.text = threadItem.aliasAndColorKey.first?.value textViewThreadDate.text = threadItem.date - textViewThreadMessageContent.text = threadItem.message textViewRepliesQuantity.text = threadItem.repliesAmount textViewThreadTime.text = threadItem.lastReplyDate + textViewThreadMessageContent.text = threadItem.message textViewThreadMessageContent.goneIfFalse(threadItem.message.isNotEmpty()) + SphinxHighlightingTool.addHighlights( + textViewThreadMessageContent, + threadItem.highlightedTexts, + textViewThreadMessageContent.resources, + textViewThreadMessageContent.context + ) + // User Profile Picture layoutLayoutChatImageSmallInitialHolder.apply { textViewInitialsName.visible diff --git a/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/model/ThreadItem.kt b/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/model/ThreadItem.kt index baa5dcf3d3..da48b65493 100644 --- a/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/model/ThreadItem.kt +++ b/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/model/ThreadItem.kt @@ -1,5 +1,6 @@ package chat.sphinx.threads.model +import androidx.annotation.IntRange import chat.sphinx.chat_common.ui.viewstate.messageholder.ReplyUserHolder import chat.sphinx.wrapper_common.FileSize import chat.sphinx.wrapper_common.PhotoUrl @@ -12,6 +13,7 @@ data class ThreadItem( val photoUrl: PhotoUrl?, val date: String, val message: String, + val highlightedTexts: List>, val usersReplies: List?, val usersCount: Int, val repliesAmount: String, diff --git a/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/ui/ThreadsViewModel.kt b/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/ui/ThreadsViewModel.kt index aaa168193b..a1f6e78aaa 100644 --- a/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/ui/ThreadsViewModel.kt +++ b/sphinx/screens/threads/threads/src/main/java/chat/sphinx/threads/ui/ThreadsViewModel.kt @@ -10,6 +10,8 @@ import chat.sphinx.chat_common.ui.viewstate.messageholder.ReplyUserHolder import chat.sphinx.concept_repository_chat.ChatRepository import chat.sphinx.concept_repository_contact.ContactRepository import chat.sphinx.concept_repository_message.MessageRepository +import chat.sphinx.highlighting_tool.highlightedTexts +import chat.sphinx.highlighting_tool.replacingHighlightedDelimiters import chat.sphinx.threads.R import chat.sphinx.threads.model.FileAttachment import chat.sphinx.threads.model.ThreadItem @@ -146,7 +148,9 @@ internal class ThreadsViewModel @Inject constructor( private suspend fun generateThreadItemsList(messages: List): List { // Group messages by their ThreadUUID - val groupedMessagesByThread = messages.groupBy { it.threadUUID } + val groupedMessagesByThread = messages.groupBy { it.threadUUID }.filter { + it.value.size > 1 + } // Fetch the header messages based on the message UUIDs val headerMessages = messageRepository.getAllMessagesByUUID(groupedMessagesByThread.keys.mapNotNull { it?.value?.toMessageUUID() }) @@ -240,12 +244,14 @@ internal class ThreadsViewModel @Inject constructor( } } + val threadMessage = originalMessage?.messageContentDecrypted?.value ?: "" return ThreadItem( aliasAndColorKey = senderInfo, photoUrl = senderPhotoUrl, date = originalMessage?.date?.chatTimeFormat() ?: "", - message = originalMessage?.messageContentDecrypted?.value ?: "", + message = threadMessage.replacingHighlightedDelimiters(), + highlightedTexts = threadMessage.highlightedTexts(), usersReplies = createReplyUserHolders(repliesList, chat, owner), usersCount = repliesList?.size ?: 0, repliesAmount = String.format(app.getString(R.string.replies_amount), messagesForThread?.drop(1)?.size?.toString() ?: "0"),