Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: search message highlight [WPB-5163] #2426

Merged
merged 5 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import com.wire.kalium.logic.data.user.UserId
fun MessageItem(
message: UIMessage.Regular,
conversationDetailsData: ConversationDetailsData,
searchQuery: String = "",
showAuthor: Boolean = true,
audioMessagesState: Map<String, AudioState>,
onLongClicked: (UIMessage.Regular) -> Unit,
Expand Down Expand Up @@ -236,6 +237,7 @@ fun MessageItem(
MessageContent(
message = message,
messageContent = messageContent,
searchQuery = searchQuery,
audioMessagesState = audioMessagesState,
onAudioClick = onAudioClick,
onChangeAudioPosition = onChangeAudioPosition,
Expand Down Expand Up @@ -463,6 +465,7 @@ private fun Username(username: String, modifier: Modifier = Modifier) {
private fun MessageContent(
message: UIMessage.Regular,
messageContent: UIMessageContent.Regular?,
searchQuery: String,
audioMessagesState: Map<String, AudioState>,
onAssetClick: Clickable,
onImageClick: Clickable,
Expand Down Expand Up @@ -498,6 +501,7 @@ private fun MessageContent(
}
MessageBody(
messageBody = messageContent.messageBody,
searchQuery = searchQuery,
isAvailable = !message.isPending && message.isAvailable,
onLongClick = onLongClick,
onOpenProfile = onOpenProfile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ internal fun MessageBody(
messageId: String,
messageBody: MessageBody?,
isAvailable: Boolean,
searchQuery: String = "",
onLongClick: (() -> Unit)? = null,
onOpenProfile: (String) -> Unit,
buttonList: List<MessageButton>?,
Expand All @@ -94,6 +95,7 @@ internal fun MessageBody(
style = MaterialTheme.wireTypography.body01,
colorScheme = MaterialTheme.wireColorScheme,
typography = MaterialTheme.wireTypography,
searchQuery = searchQuery,
mentions = displayMentions,
onLongClick = onLongClick,
onOpenProfile = onOpenProfile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ package com.wire.android.ui.home.conversations.search
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
Expand All @@ -39,31 +33,22 @@ import com.wire.android.R
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.EMPTY
import com.wire.android.util.MatchQueryResult
import com.wire.android.util.QueryMatchExtractor
import kotlinx.coroutines.launch

@Composable
fun HighlightName(
name: String,
searchQuery: String,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
var highlightIndexes by remember {
mutableStateOf(emptyList<MatchQueryResult>())
}

val queryWithoutSuffix = searchQuery.removeQueryPrefix()

SideEffect {
scope.launch {
highlightIndexes = QueryMatchExtractor.extractQueryMatchIndexes(
matchText = queryWithoutSuffix,
text = name
)
}
}
val highlightIndexes = QueryMatchExtractor.extractQueryMatchIndexes(
matchText = queryWithoutSuffix,
text = name
)

if (queryWithoutSuffix != String.EMPTY && highlightIndexes.isNotEmpty()) {
Text(
buildAnnotatedString {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,27 @@ package com.wire.android.ui.home.conversations.search
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.EMPTY
import com.wire.android.util.MatchQueryResult
import com.wire.android.util.QueryMatchExtractor
import kotlinx.coroutines.launch

@Composable
fun HighlightSubtitle(
subTitle: String,
searchQuery: String = "",
suffix: String = "@"
) {
val scope = rememberCoroutineScope()
var highlightIndexes by remember {
mutableStateOf(emptyList<MatchQueryResult>())
}

val queryWithoutSuffix = searchQuery.removeQueryPrefix()

SideEffect {
scope.launch {
highlightIndexes = QueryMatchExtractor.extractQueryMatchIndexes(
matchText = queryWithoutSuffix,
text = subTitle
)
}
}
val highlightIndexes = QueryMatchExtractor.extractQueryMatchIndexes(
matchText = queryWithoutSuffix,
text = subTitle
)

if (queryWithoutSuffix != String.EMPTY && highlightIndexes.isNotEmpty()) {
Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes
@Composable
fun SearchConversationMessagesResultsScreen(
searchResult: List<UIMessage>,
searchQuery: String = "",
onMessageClick: (messageId: String) -> Unit
) {
LazyColumn {
Expand All @@ -40,6 +41,7 @@ fun SearchConversationMessagesResultsScreen(
MessageItem(
message = message,
conversationDetailsData = ConversationDetailsData.None,
searchQuery = searchQuery,
audioMessagesState = mapOf(),
onLongClicked = { },
onAssetMessageClicked = { },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ fun SearchConversationMessagesResultContent(
} else {
SearchConversationMessagesResultsScreen(
searchResult = searchResult,
searchQuery = searchQuery,
onMessageClick = onMessageClick
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ class SearchConversationMessagesViewModel @Inject constructor(
val textQueryChanged = searchConversationMessagesState.searchQuery.text != searchQuery.text
// we set the state with a searchQuery, immediately to update the UI first
searchConversationMessagesState = searchConversationMessagesState.copy(searchQuery = searchQuery)
if (textQueryChanged) {
if (textQueryChanged && searchQuery.text.isNotBlank()) {
viewModelScope.launch {
mutableSearchQueryFlow.emit(searchQuery.text)
mutableSearchQueryFlow.emit(searchQuery.text.trim())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import androidx.compose.ui.text.style.TextDecoration
import com.wire.android.ui.common.LinkSpannableString
import com.wire.android.ui.markdown.MarkdownConstants.MENTION_MARK
import com.wire.android.ui.markdown.MarkdownConstants.TAG_URL
import com.wire.android.util.MatchQueryResult
import com.wire.android.util.QueryMatchExtractor
import com.wire.kalium.logic.data.message.mention.MessageMention
import org.commonmark.ext.gfm.strikethrough.Strikethrough
import org.commonmark.ext.gfm.tables.TableBlock
Expand Down Expand Up @@ -203,6 +205,7 @@ fun appendLinksAndMentions(

val stringBuilder = StringBuilder(string)
val updatedMentions = nodeData.mentions.toMutableList()
var highlightIndexes = emptyList<MatchQueryResult>()

// get mentions from text, remove mention marks and update position of mentions
val mentionList: List<MessageMention> = if (stringBuilder.contains(MENTION_MARK) && updatedMentions.isNotEmpty()) {
Expand All @@ -229,6 +232,13 @@ fun appendLinksAndMentions(
listOf()
}

if (nodeData.searchQuery.isNotBlank()) {
highlightIndexes = QueryMatchExtractor.extractQueryMatchIndexes(
matchText = nodeData.searchQuery,
text = stringBuilder.toString()
)
}

val linkInfos = LinkSpannableString.getLinkInfos(stringBuilder.toString(), Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES)

val append = buildAnnotatedString {
Expand Down Expand Up @@ -257,27 +267,42 @@ fun appendLinksAndMentions(
}

if (mentionList.isNotEmpty()) {
mentionList.forEach {
if (it.length <= 0 || it.start >= length || it.start + it.length > length) {
mentionList.forEach { mention ->
if (mention.length <= 0 || mention.start >= length || mention.start + mention.length > length) {
return@forEach
}
addStyle(
style = SpanStyle(
fontWeight = nodeData.typography.body02.fontWeight,
color = onPrimaryVariant,
background = if (it.isSelfMention) primaryVariant else Color.Unspecified
background = if (mention.isSelfMention) primaryVariant else Color.Unspecified
),
start = it.start,
end = it.start + it.length
start = mention.start,
end = mention.start + mention.length
)
addStringAnnotation(
tag = MarkdownConstants.TAG_MENTION,
annotation = it.userId.toString(),
start = it.start,
end = it.start + it.length
annotation = mention.userId.toString(),
start = mention.start,
end = mention.start + mention.length
)
}
}

highlightIndexes
.forEach { highLightIndex ->
if (highLightIndex.endIndex <= length) {
addStyle(
style = SpanStyle(
background = nodeData.colorScheme.highLight.copy(alpha = 0.5f),
fontFamily = nodeData.typography.body02.fontFamily,
fontWeight = FontWeight.Bold
),
start = highLightIndex.startIndex,
end = highLightIndex.endIndex
)
}
}
}
}
annotatedString.append(append)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ data class NodeData(
val colorScheme: WireColorScheme,
val typography: WireTypography,
val mentions: List<DisplayMention>,
val searchQuery: String,
val onLongClick: (() -> Unit)? = null,
val onOpenProfile: (String) -> Unit,
val onLinkClick: (String) -> Unit
Expand Down
48 changes: 22 additions & 26 deletions app/src/main/kotlin/com/wire/android/util/QueryMatchExtractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,41 @@

package com.wire.android.util

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

object QueryMatchExtractor {
/**
* extractHighLightIndexes is a recursive function returning a list of the start index and end index
* of the [matchText] that is a text we want to search within the String provided as [text].
* [resultMatches] contains a list of QueryResult with startIndex and endIndex if the match is found.
* [startIndex] is a index from which we start the search
*/
suspend fun extractQueryMatchIndexes(
fun extractQueryMatchIndexes(
resultMatches: List<MatchQueryResult> = emptyList(),
startIndex: Int = 0,
matchText: String,
text: String
): List<MatchQueryResult> =
withContext(Dispatchers.Default) {
if (matchText.isEmpty()) {
return@withContext listOf()
}
val index = text.indexOf(matchText, startIndex = startIndex, ignoreCase = true)
): List<MatchQueryResult> {
if (matchText.isEmpty()) {
return listOf()
}
val index = text.indexOf(matchText, startIndex = startIndex, ignoreCase = true)

if (isIndexFound(index)) {
extractQueryMatchIndexes(
resultMatches = resultMatches + MatchQueryResult(
startIndex = index,
endIndex = index + matchText.length
),
// we are incrementing the startIndex by 1 for the next recursion
// to start looking for the match from the next index that we ended up
// finding the match for the matchText
startIndex = index + 1,
matchText = matchText,
text = text
)
} else {
resultMatches
}
return if (isIndexFound(index)) {
extractQueryMatchIndexes(
resultMatches = resultMatches + MatchQueryResult(
startIndex = index,
endIndex = index + matchText.length
),
// we are incrementing the startIndex by 1 for the next recursion
// to start looking for the match from the next index that we ended up
// finding the match for the matchText
startIndex = index + 1,
matchText = matchText,
text = text
)
} else {
resultMatches
}
}

private fun isIndexFound(index: Int): Boolean {
return index != -1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ object FeatureVisibilityFlags {
const val ConversationSearchIcon = false
const val UserProfileEditIcon = false
const val MessageEditIcon = true
const val SearchConversationMessages = false
const val SearchConversationMessages = true
}

val LocalFeatureVisibilityFlags = staticCompositionLocalOf { FeatureVisibilityFlags }
Loading