diff --git a/CHANGES.md b/CHANGES.md index 5cf2e7f53b..5ef46e5e90 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index c917c4557d..c6789e1ac9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -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() { @@ -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) { + /** + * @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): 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 } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index 7896e93cd6..a3e5983c3a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -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() { @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() { 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() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt index f61bcbd029..ab440f6b5f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt index 9f700b6e31..41fecbb5e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.search.EventAndSender data class SearchViewState( // Accumulated search result val searchResult: List = emptyList(), + val highlights: List = emptyList(), val hasMoreResult: Boolean = false, // Last batch size, will help RecyclerView to position itself val lastBatchSize: Int = 0,