Merge branch 'develop' into feature/bma/changelog

This commit is contained in:
Benoit Marty 2020-10-30 14:01:35 +01:00 committed by GitHub
commit c38a8599f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 91 additions and 37 deletions

View File

@ -16,9 +16,11 @@ Improvements 🙌:
- Room member profile: Add action to create (or open) a DM (#2310)
- Prepare changelog for F-Droid (#2296)
- Add graphic resources for F-Droid (#812, #2220)
- 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)
- Search Result | scroll jumps after pagination (#2238)
Translations 🗣:
-

View File

@ -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,10 +92,8 @@ class SearchFragment @Inject constructor(
}
}
} else {
pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0)
stateView.state = StateView.State.Content
controller.setData(state)
stateView.state = StateView.State.Content
}
}

View File

@ -16,16 +16,24 @@
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.EpoxyModel
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.ui.list.genericItemHeader
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 +41,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>() {
@ -52,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
@ -62,35 +73,85 @@ class SearchResultController @Inject constructor(
}
}
}
} 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))
}
}
}
buildSearchResultItems(data.searchResult)
searchItems.forEach { add(it) }
}
private fun buildSearchResultItems(events: List<EventAndSender>) {
/**
* @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<EpoxyModel<*>> {
var lastDate: Calendar? = null
val result = mutableListOf<EpoxyModel<*>>()
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()
}
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)
dateFormatter(dateFormatter)
event(eventAndSender.event)
sender(eventAndSender.sender
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) }
.listener { listener?.onItemClicked(eventAndSender.event) }
.let { result.add(it) }
}
return result
}
/**
* 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,23 +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.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
@ -47,9 +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.)
holder.contentView.text = 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,

View File

@ -8,8 +8,10 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:overScrollMode="always"
tools:itemCount="2"
tools:listitem="@layout/item_search_result" />
</im.vector.app.core.platform.StateView>

View File

@ -173,6 +173,7 @@
<string name="no_conversation_placeholder">No conversations</string>
<string name="no_contact_access_placeholder">You didnt allow Element to access your local contacts</string>
<string name="no_result_placeholder">No results</string>
<string name="no_more_results">No more results</string>
<string name="people_no_identity_server">No identity server configured.</string>
<!-- Rooms fragment -->