mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge branch 'develop' into feature/bma/changelog
This commit is contained in:
commit
c38a8599f1
8 changed files with 91 additions and 37 deletions
|
@ -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 🗣:
|
||||
-
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -173,6 +173,7 @@
|
|||
<string name="no_conversation_placeholder">No conversations</string>
|
||||
<string name="no_contact_access_placeholder">You didn’t 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 -->
|
||||
|
|
Loading…
Reference in a new issue