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)
|
- 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 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)
|
- Room member profile: Add action to create (or open) a DM (#2310)
|
||||||
|
- Highlight text in the body of the displayed result (#2200)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
|
- 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
|
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.TypedEpoxyController
|
||||||
import com.airbnb.epoxy.VisibilityState
|
import com.airbnb.epoxy.VisibilityState
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.date.DateFormatKind
|
import im.vector.app.core.date.DateFormatKind
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
import im.vector.app.core.epoxy.loadingItem
|
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.core.ui.list.genericItemHeader
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.search.EventAndSender
|
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -33,6 +40,7 @@ import javax.inject.Inject
|
|||||||
class SearchResultController @Inject constructor(
|
class SearchResultController @Inject constructor(
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
private val dateFormatter: VectorDateFormatter
|
private val dateFormatter: VectorDateFormatter
|
||||||
) : TypedEpoxyController<SearchViewState>() {
|
) : 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 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 {
|
val eventDate = Calendar.getInstance().apply {
|
||||||
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
|
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
@ -85,12 +111,39 @@ class SearchResultController @Inject constructor(
|
|||||||
searchResultItem {
|
searchResultItem {
|
||||||
id(eventAndSender.event.eventId)
|
id(eventAndSender.event.eventId)
|
||||||
avatarRenderer(avatarRenderer)
|
avatarRenderer(avatarRenderer)
|
||||||
dateFormatter(dateFormatter)
|
formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
|
||||||
event(eventAndSender.event)
|
spannable(spannable)
|
||||||
sender(eventAndSender.sender
|
sender(eventAndSender.sender
|
||||||
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
|
?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
|
||||||
listener { listener?.onItemClicked(eventAndSender.event) }
|
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.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
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.ClickListener
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_search_result)
|
@EpoxyModelClass(layout = R.layout.item_search_result)
|
||||||
abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
|
abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
@EpoxyAttribute var dateFormatter: VectorDateFormatter? = null
|
@EpoxyAttribute var formattedDate: String? = null
|
||||||
@EpoxyAttribute lateinit var event: Event
|
@EpoxyAttribute lateinit var spannable: CharSequence
|
||||||
@EpoxyAttribute var sender: MatrixItem? = null
|
@EpoxyAttribute var sender: MatrixItem? = null
|
||||||
@EpoxyAttribute var listener: ClickListener? = null
|
@EpoxyAttribute var listener: ClickListener? = null
|
||||||
|
|
||||||
@ -48,11 +44,8 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
|
|||||||
holder.view.onClick(listener)
|
holder.view.onClick(listener)
|
||||||
sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
|
sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
|
||||||
holder.memberNameView.setTextOrHide(sender?.getBestName())
|
holder.memberNameView.setTextOrHide(sender?.getBestName())
|
||||||
holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
holder.timeView.text = formattedDate
|
||||||
// TODO Improve that (use formattedBody, etc.)
|
holder.contentView.text = spannable
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
// Take new content first
|
|
||||||
holder.contentView.text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -145,6 +145,7 @@ class SearchViewModel @AssistedInject constructor(
|
|||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
searchResult = accumulatedResult,
|
searchResult = accumulatedResult,
|
||||||
|
highlights = searchResult.highlights.orEmpty(),
|
||||||
hasMoreResult = !nextBatch.isNullOrEmpty(),
|
hasMoreResult = !nextBatch.isNullOrEmpty(),
|
||||||
lastBatchSize = searchResult.results.orEmpty().size,
|
lastBatchSize = searchResult.results.orEmpty().size,
|
||||||
asyncSearchRequest = Success(Unit)
|
asyncSearchRequest = Success(Unit)
|
||||||
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.search.EventAndSender
|
|||||||
data class SearchViewState(
|
data class SearchViewState(
|
||||||
// Accumulated search result
|
// Accumulated search result
|
||||||
val searchResult: List<EventAndSender> = emptyList(),
|
val searchResult: List<EventAndSender> = emptyList(),
|
||||||
|
val highlights: List<String> = emptyList(),
|
||||||
val hasMoreResult: Boolean = false,
|
val hasMoreResult: Boolean = false,
|
||||||
// Last batch size, will help RecyclerView to position itself
|
// Last batch size, will help RecyclerView to position itself
|
||||||
val lastBatchSize: Int = 0,
|
val lastBatchSize: Int = 0,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user