Highlight text in the body of the displayed result (#2200)
This commit is contained in:
parent
403e18c1b7
commit
5f99eb8c97
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue