Merge branch 'develop' into feature/fix_sync_after_call_invite

This commit is contained in:
Benoit Marty 2020-11-02 14:45:05 +01:00 committed by GitHub
commit 51ae2ce1e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 179 additions and 69 deletions

View File

@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element. We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
Feel free to add your name below, when you contribute to the project! Feel free to add your name below, when you contribute to the project!
Name | Matrix ID | GitHub
--------|---------------------|--------------------------------------
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)

View File

@ -14,10 +14,16 @@ Improvements 🙌:
- 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
- Better visibility of text reactions in dark theme (#1118) - Better visibility of text reactions in dark theme (#1118)
- Room member profile: Add action to create (or open) a DM (#2310) - 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)
- Considerably faster QR-code bitmap generation (#2331)
Bugfix 🐛: Bugfix 🐛:
- Fixed ringtone handling (#2100 & #2246)
- 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)
- Incoming call continues to ring if call is answered on another device (#1921) - Incoming call continues to ring if call is answered on another device (#1921)
- Search Result | scroll jumps after pagination (#2238)
Translations 🗣: Translations 🗣:
- -

View File

@ -0,0 +1 @@
// TODO

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View File

@ -167,11 +167,14 @@ internal class DefaultTimeline(
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
?: throw IllegalStateException("Can't open a timeline without a room") ?: throw IllegalStateException("Can't open a timeline without a room")
sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() // We don't want to filter here because some sending events that are not displayed
// are still used for ui echo (relation like reaction)
sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll()
sendingEvents.addChangeListener { events -> sendingEvents.addChangeListener { events ->
uiEchoManager.sentEventsUpdated(events) uiEchoManager.sentEventsUpdated(events)
postSnapshot() postSnapshot()
} }
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where() filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings() .filterEventsWithSettings()
@ -410,13 +413,16 @@ internal class DefaultTimeline(
val builtSendingEvents = ArrayList<TimelineEvent>() val builtSendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings()) builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
sendingEvents.forEach { timelineEventEntity -> sendingEvents
if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { .map { timelineEventMapper.map(it) }
val element = timelineEventMapper.map(timelineEventEntity) // Filter out sending event that are not displayable!
uiEchoManager.updateSentStateWithUiEcho(element) .filterEventsWithSettings()
builtSendingEvents.add(element) .forEach { timelineEvent ->
} if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) {
} uiEchoManager.updateSentStateWithUiEcho(timelineEvent)
builtSendingEvents.add(timelineEvent)
}
}
} }
return builtSendingEvents return builtSendingEvents
} }
@ -782,8 +788,8 @@ internal class DefaultTimeline(
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type) val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
if (!filterType) return@filter false if (!filterType) return@filter false
val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) { val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) {
val messageContent = it.root.content.toModel<MessageContent>() val messageContent = it.root.getClearContent().toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else { } else {
true true
@ -813,7 +819,7 @@ internal class DefaultTimeline(
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
fun getInMemorySendingEvents(): List<TimelineEvent> { fun getInMemorySendingEvents(): List<TimelineEvent> {
return inMemorySendingEvents return inMemorySendingEvents.toList()
} }
/** /**
@ -831,8 +837,13 @@ internal class DefaultTimeline(
inMemorySendingStates.keys.removeAll { key -> inMemorySendingStates.keys.removeAll { key ->
events.find { it.eventId == key } == null events.find { it.eventId == key } == null
} }
inMemoryReactions.keys.removeAll { key ->
events.find { it.eventId == key } == null inMemoryReactions.forEach { (_, uiEchoData) ->
uiEchoData.removeAll { data ->
// I remove the uiEcho, when the related event is not anymore in the sending list
// (means that it is synced)!
events.find { it.eventId == data.localEchoId } == null
}
} }
} }
@ -896,6 +907,7 @@ internal class DefaultTimeline(
relatedEventID relatedEventID
) )
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList() val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
contents.forEach { uiEchoReaction -> contents.forEach { uiEchoReaction ->
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction } val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
if (existing == null) { if (existing == null) {

View File

@ -34,12 +34,15 @@ fun String.toBitMatrix(size: Int): BitMatrix {
fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE, fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
@ColorInt foregroundColor: Int = Color.BLACK): Bitmap { @ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val colorBuffer = IntArray(width * height)
for (x in 0 until width) { var rowOffset = 0
for (y in 0 until height) { for (y in 0 until height) {
bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor) for (x in 0 until width) {
val arrayIndex = x + rowOffset
colorBuffer[arrayIndex] = if (get(x, y)) foregroundColor else backgroundColor
} }
rowOffset += width
} }
return bmp return Bitmap.createBitmap(colorBuffer, width, height, Bitmap.Config.ARGB_8888)
} }

View File

@ -17,6 +17,8 @@
package im.vector.app.core.services package im.vector.app.core.services
import android.content.Context import android.content.Context
import android.media.Ringtone
import android.media.RingtoneManager
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
@ -25,7 +27,26 @@ import androidx.core.content.getSystemService
import im.vector.app.R import im.vector.app.R
import timber.log.Timber import timber.log.Timber
class CallRingPlayer( class CallRingPlayerIncoming(
context: Context
) {
private val applicationContext = context.applicationContext
private var r: Ringtone? = null
fun start() {
val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
r = RingtoneManager.getRingtone(applicationContext, notification)
Timber.v("## VOIP Starting ringing incomming")
r?.play()
}
fun stop() {
r?.stop()
}
}
class CallRingPlayerOutgoing(
context: Context context: Context
) { ) {
@ -44,12 +65,12 @@ class CallRingPlayer(
try { try {
if (player?.isPlaying == false) { if (player?.isPlaying == false) {
player?.start() player?.start()
Timber.v("## VOIP Starting ringing") Timber.v("## VOIP Starting ringing outgoing")
} else { } else {
Timber.v("## VOIP already playing") Timber.v("## VOIP already playing")
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## VOIP Failed to start ringing") Timber.e(failure, "## VOIP Failed to start ringing outgoing")
player = null player = null
} }
} else { } else {
@ -74,7 +95,7 @@ class CallRingPlayer(
} else { } else {
mediaPlayer.setAudioAttributes(AudioAttributes.Builder() mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build()) .build())
} }
return mediaPlayer return mediaPlayer

View File

@ -40,7 +40,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
private lateinit var notificationUtils: NotificationUtils private lateinit var notificationUtils: NotificationUtils
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
private var callRingPlayer: CallRingPlayer? = null private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
@ -63,14 +64,16 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
super.onCreate() super.onCreate()
notificationUtils = vectorComponent().notificationUtils() notificationUtils = vectorComponent().notificationUtils()
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager() webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
callRingPlayer = CallRingPlayer(applicationContext) callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this) bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
callRingPlayer?.stop() callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) } wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
wiredHeadsetStateReceiver = null wiredHeadsetStateReceiver = null
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) } bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
@ -100,16 +103,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
when (intent.action) { when (intent.action) {
ACTION_INCOMING_RINGING_CALL -> { ACTION_INCOMING_RINGING_CALL -> {
mediaSession?.isActive = true mediaSession?.isActive = true
callRingPlayer?.start() callRingPlayerIncoming?.start()
displayIncomingCallNotification(intent) displayIncomingCallNotification(intent)
} }
ACTION_OUTGOING_RINGING_CALL -> { ACTION_OUTGOING_RINGING_CALL -> {
mediaSession?.isActive = true mediaSession?.isActive = true
callRingPlayer?.start() callRingPlayerOutgoing?.start()
displayOutgoingRingingCallNotification(intent) displayOutgoingRingingCallNotification(intent)
} }
ACTION_ONGOING_CALL -> { ACTION_ONGOING_CALL -> {
callRingPlayer?.stop() callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
displayCallInProgressNotification(intent) displayCallInProgressNotification(intent)
} }
ACTION_NO_ACTIVE_CALL -> hideCallNotifications() ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
@ -117,7 +121,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
// lower notification priority // lower notification priority
displayCallInProgressNotification(intent) displayCallInProgressNotification(intent)
// stop ringing // stop ringing
callRingPlayer?.stop() callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
} }
ACTION_ONGOING_CALL_BG -> { ACTION_ONGOING_CALL_BG -> {
// there is an ongoing call but call activity is in background // there is an ongoing call but call activity is in background
@ -125,7 +130,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
} }
else -> { else -> {
// Should not happen // Should not happen
callRingPlayer?.stop() callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
myStopSelf() myStopSelf()
} }
} }

View File

@ -93,6 +93,10 @@ class CallAudioManager(
fun startForCall(mxCall: MxCall) { fun startForCall(mxCall: MxCall) {
Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}") Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}")
}
private fun setupAudioManager(mxCall: MxCall) {
Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}")
val audioManager = audioManager ?: return val audioManager = audioManager ?: return
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
savedIsMicrophoneMute = audioManager.isMicrophoneMute savedIsMicrophoneMute = audioManager.isMicrophoneMute
@ -150,7 +154,7 @@ class CallAudioManager(
fun onCallConnected(mxCall: MxCall) { fun onCallConnected(mxCall: MxCall) {
Timber.v("##VOIP: AudioManager call answered, adjusting current sound device") Timber.v("##VOIP: AudioManager call answered, adjusting current sound device")
adjustCurrentSoundDevice(mxCall) setupAudioManager(mxCall)
} }
fun getAvailableSoundDevices(): List<SoundDevice> { fun getAvailableSoundDevices(): List<SoundDevice> {

View File

@ -52,8 +52,6 @@ class SearchFragment @Inject constructor(
private val fragmentArgs: SearchArgs by args() private val fragmentArgs: SearchArgs by args()
private val searchViewModel: SearchViewModel by fragmentViewModel() private val searchViewModel: SearchViewModel by fragmentViewModel()
private var pendingScrollToPosition: Int? = null
override fun getLayoutResId() = R.layout.fragment_search override fun getLayoutResId() = R.layout.fragment_search
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -70,12 +68,6 @@ class SearchFragment @Inject constructor(
searchResultRecycler.configureWith(controller, showDivider = false) searchResultRecycler.configureWith(controller, showDivider = false)
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true (searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
controller.listener = this controller.listener = this
controller.addModelBuildListener {
pendingScrollToPosition?.let {
searchResultRecycler.smoothScrollToPosition(it)
}
}
} }
override fun onDestroy() { override fun onDestroy() {
@ -100,10 +92,8 @@ class SearchFragment @Inject constructor(
} }
} }
} else { } else {
pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0)
stateView.state = StateView.State.Content
controller.setData(state) controller.setData(state)
stateView.state = StateView.State.Content
} }
} }

View File

@ -16,16 +16,24 @@
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.EpoxyModel
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.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 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 +41,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>() {
@ -52,6 +61,8 @@ class SearchResultController @Inject constructor(
override fun buildModels(data: SearchViewState?) { override fun buildModels(data: SearchViewState?) {
data ?: return data ?: return
val searchItems = buildSearchResultItems(data)
if (data.hasMoreResult) { if (data.hasMoreResult) {
loadingItem { loadingItem {
// Always use a different id, because we can be notified several times of visibility state changed // 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 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 { val eventDate = Calendar.getInstance().apply {
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis() timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
} }
if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) { if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) {
genericItemHeader { GenericItemHeader_()
id(eventDate.hashCode()) .id(eventDate.hashCode())
text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER)) .text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
} .let { result.add(it) }
} }
lastDate = eventDate lastDate = eventDate
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) }
.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.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.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
@ -47,9 +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
holder.contentView.text = event.content?.get("body") as? String
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -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)

View File

@ -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,

View File

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

View File

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