Highlight text in the body of the displayed result (#2200)

This commit is contained in:
Benoit Marty 2020-10-29 19:13:35 +01:00
parent 403e18c1b7
commit 5f99eb8c97
5 changed files with 66 additions and 17 deletions

View File

@ -13,6 +13,7 @@ Improvements 🙌:
- Edit and remove icons are now visible on image attachment preview screen (#2294)
- Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
- Room member profile: Add action to create (or open) a DM (#2310)
- Highlight text in the body of the displayed result (#2200)
Bugfix 🐛:
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)

View File

@ -16,16 +16,23 @@
package im.vector.app.features.home.room.detail.search
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.search.EventAndSender
import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Calendar
import javax.inject.Inject
@ -33,6 +40,7 @@ import javax.inject.Inject
class SearchResultController @Inject constructor(
private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<SearchViewState>() {
@ -64,13 +72,31 @@ class SearchResultController @Inject constructor(
}
}
buildSearchResultItems(data.searchResult)
val hasItems = buildSearchResultItems(data)
if (!hasItems && !data.hasMoreResult) {
// All returned result returned by the server has been filtered out and there is no more result
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.no_result_placeholder))
}
}
}
private fun buildSearchResultItems(events: List<EventAndSender>) {
/**
* @return true if some item has been added
*/
private fun buildSearchResultItems(data: SearchViewState): Boolean {
var lastDate: Calendar? = null
var hasItems = false
data.searchResult.forEach { eventAndSender ->
val event = eventAndSender.event
@Suppress("UNCHECKED_CAST")
// Take new content first
val text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String ?: return@forEach
val spannable = setHighLightedText(text, data.highlights) ?: return@forEach
events.forEach { eventAndSender ->
val eventDate = Calendar.getInstance().apply {
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
}
@ -85,12 +111,39 @@ class SearchResultController @Inject constructor(
searchResultItem {
id(eventAndSender.event.eventId)
avatarRenderer(avatarRenderer)
dateFormatter(dateFormatter)
event(eventAndSender.event)
formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
spannable(spannable)
sender(eventAndSender.sender
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
listener { listener?.onItemClicked(eventAndSender.event) }
}
hasItems = true
}
return hasItems
}
/**
* Highlight the text. If the text is not found, return null to ignore this result
* See https://github.com/matrix-org/synapse/issues/8686
*/
private fun setHighLightedText(text: String, highlights: List<String>): Spannable? {
val wordToSpan: Spannable = SpannableString(text)
var found = false
highlights.forEach { highlight ->
var searchFromIndex = 0
while (searchFromIndex < text.length) {
val indexOfHighlight = text.indexOf(highlight, searchFromIndex, ignoreCase = true)
searchFromIndex = if (indexOfHighlight == -1) {
Integer.MAX_VALUE
} else {
// bold
found = true
wordToSpan.setSpan(StyleSpan(Typeface.BOLD), indexOfHighlight, indexOfHighlight + highlight.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
indexOfHighlight + 1
}
}
}
return wordToSpan.takeIf { found }
}
}

View File

@ -21,24 +21,20 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_search_result)
abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var dateFormatter: VectorDateFormatter? = null
@EpoxyAttribute lateinit var event: Event
@EpoxyAttribute var formattedDate: String? = null
@EpoxyAttribute lateinit var spannable: CharSequence
@EpoxyAttribute var sender: MatrixItem? = null
@EpoxyAttribute var listener: ClickListener? = null
@ -48,11 +44,8 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
holder.view.onClick(listener)
sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
holder.memberNameView.setTextOrHide(sender?.getBestName())
holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
// TODO Improve that (use formattedBody, etc.)
@Suppress("UNCHECKED_CAST")
// Take new content first
holder.contentView.text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String
holder.timeView.text = formattedDate
holder.contentView.text = spannable
}
class Holder : VectorEpoxyHolder() {

View File

@ -145,6 +145,7 @@ class SearchViewModel @AssistedInject constructor(
setState {
copy(
searchResult = accumulatedResult,
highlights = searchResult.highlights.orEmpty(),
hasMoreResult = !nextBatch.isNullOrEmpty(),
lastBatchSize = searchResult.results.orEmpty().size,
asyncSearchRequest = Success(Unit)

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.search.EventAndSender
data class SearchViewState(
// Accumulated search result
val searchResult: List<EventAndSender> = emptyList(),
val highlights: List<String> = emptyList(),
val hasMoreResult: Boolean = false,
// Last batch size, will help RecyclerView to position itself
val lastBatchSize: Int = 0,