From 403e18c1b72faa499de2dab7dd638c7b2f3e0351 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 29 Oct 2020 17:30:25 +0100 Subject: [PATCH 1/6] Search result: take new content for edited message --- .../app/features/home/room/detail/search/SearchResultItem.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 10407c64e0..7896e93cd6 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 @@ -29,6 +29,7 @@ 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 @@ -49,7 +50,9 @@ abstract class SearchResultItem : VectorEpoxyModel() { holder.memberNameView.setTextOrHide(sender?.getBestName()) holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE) // TODO Improve that (use formattedBody, etc.) - holder.contentView.text = event.content?.get("body") as? String + @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() { From 5f99eb8c9796f0cd00f0c05519e8a5847ba0aac8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 29 Oct 2020 19:13:35 +0100 Subject: [PATCH 2/6] Highlight text in the body of the displayed result (#2200) --- CHANGES.md | 1 + .../detail/search/SearchResultController.kt | 65 +++++++++++++++++-- .../room/detail/search/SearchResultItem.kt | 15 ++--- .../room/detail/search/SearchViewModel.kt | 1 + .../room/detail/search/SearchViewState.kt | 1 + 5 files changed, 66 insertions(+), 17 deletions(-) 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, From 225a5d4e59f81c316f5e2c4565215b645a838a97 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Oct 2020 09:02:29 +0100 Subject: [PATCH 3/6] Search Result | scroll jumps after pagination (#2238) --- CHANGES.md | 1 + .../features/home/room/detail/search/SearchFragment.kt | 10 ---------- vector/src/main/res/layout/fragment_search.xml | 4 +++- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5ef46e5e90..02580338d6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ Improvements 🙌: Bugfix 🐛: - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252) + - Search Result | scroll jumps after pagination (#2238) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 10dc9254d8..6528f0b12b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -52,8 +52,6 @@ class SearchFragment @Inject constructor( private val fragmentArgs: SearchArgs by args() private val searchViewModel: SearchViewModel by fragmentViewModel() - private var pendingScrollToPosition: Int? = null - override fun getLayoutResId() = R.layout.fragment_search override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -70,12 +68,6 @@ class SearchFragment @Inject constructor( searchResultRecycler.configureWith(controller, showDivider = false) (searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true controller.listener = this - - controller.addModelBuildListener { - pendingScrollToPosition?.let { - searchResultRecycler.smoothScrollToPosition(it) - } - } } override fun onDestroy() { @@ -100,8 +92,6 @@ class SearchFragment @Inject constructor( } } } else { - pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0) - stateView.state = StateView.State.Content controller.setData(state) } diff --git a/vector/src/main/res/layout/fragment_search.xml b/vector/src/main/res/layout/fragment_search.xml index 330e70d86b..84547a4355 100644 --- a/vector/src/main/res/layout/fragment_search.xml +++ b/vector/src/main/res/layout/fragment_search.xml @@ -8,8 +8,10 @@ \ No newline at end of file From 316f26ec82944f6defbdb2296f0aaae5614747a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Oct 2020 09:06:48 +0100 Subject: [PATCH 4/6] Search: Add "No more results" item (iso Element Web) --- .../detail/search/SearchResultController.kt | 60 +++++++++++-------- vector/src/main/res/values/strings.xml | 1 + 2 files changed, 35 insertions(+), 26 deletions(-) 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 c6789e1ac9..a2a9e6ae7d 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 @@ -20,6 +20,7 @@ import android.graphics.Typeface import android.text.Spannable import android.text.SpannableString import android.text.style.StyleSpan +import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.VisibilityState import im.vector.app.R @@ -28,7 +29,7 @@ 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.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 @@ -60,6 +61,8 @@ class SearchResultController @Inject constructor( override fun buildModels(data: SearchViewState?) { data ?: return + val searchItems = buildSearchResultItems(data) + if (data.hasMoreResult) { loadingItem { // Always use a different id, because we can be notified several times of visibility state changed @@ -70,24 +73,30 @@ class SearchResultController @Inject constructor( } } } - } - - 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)) + } else { + if (searchItems.isEmpty()) { + // All returned results by the server has been filtered out and there is no more result + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + noResultItem { + id("noMoreResult") + text(stringProvider.getString(R.string.no_more_results)) + } } } + + searchItems.forEach { add(it) } } /** * @return true if some item has been added */ - private fun buildSearchResultItems(data: SearchViewState): Boolean { + private fun buildSearchResultItems(data: SearchViewState): List> { var lastDate: Calendar? = null - var hasItems = false + val result = mutableListOf>() data.searchResult.forEach { eventAndSender -> val event = eventAndSender.event @@ -101,26 +110,25 @@ class SearchResultController @Inject constructor( timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis() } if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) { - genericItemHeader { - id(eventDate.hashCode()) - text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER)) - } + GenericItemHeader_() + .id(eventDate.hashCode()) + .text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER)) + .let { result.add(it) } } lastDate = eventDate - searchResultItem { - id(eventAndSender.event.eventId) - avatarRenderer(avatarRenderer) - 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 + SearchResultItem_() + .id(eventAndSender.event.eventId) + .avatarRenderer(avatarRenderer) + .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) } + .let { result.add(it) } } - return hasItems + return result } /** diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 16ebca5f20..b4c42b276b 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -173,6 +173,7 @@ No conversations You didn’t allow Element to access your local contacts No results + No more results No identity server configured. From 46e5d23f7e9de7e145f8ff0629469c7edd918b39 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Oct 2020 09:36:58 +0100 Subject: [PATCH 5/6] Fix refresh screen issue --- .../app/features/home/room/detail/search/SearchFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 6528f0b12b..201e9a4f82 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -92,8 +92,8 @@ class SearchFragment @Inject constructor( } } } else { - stateView.state = StateView.State.Content controller.setData(state) + stateView.state = StateView.State.Content } } From 78170b70525eae1ff6e2858ba30c9f3e2ee0a430 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Oct 2020 09:44:47 +0100 Subject: [PATCH 6/6] Self-review --- .../features/home/room/detail/search/SearchResultController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a2a9e6ae7d..b927fb5ff3 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 @@ -92,7 +92,7 @@ class SearchResultController @Inject constructor( } /** - * @return true if some item has been added + * @return the list of EpoxyModel (date items and search result items), or an empty list if all items have been filtered out */ private fun buildSearchResultItems(data: SearchViewState): List> { var lastDate: Calendar? = null