diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt new file mode 100644 index 0000000000..74ec10b0db --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.rx + +import im.vector.matrix.android.api.util.Optional +import io.reactivex.Observable + +fun Observable>.unwrap(): Observable { + return filter { it.hasValue() }.map { it.get() } +} \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index d83f2b738f..b3915568e5 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -28,7 +28,7 @@ import io.reactivex.Single class RxRoom(private val room: Room) { - fun liveRoomSummary(): Observable { + fun liveRoomSummary(): Observable> { return room.getRoomSummaryLive().asObservable() } @@ -36,11 +36,11 @@ class RxRoom(private val room: Room) { return room.getRoomMemberIdsLive().asObservable() } - fun liveAnnotationSummary(eventId: String): Observable { + fun liveAnnotationSummary(eventId: String): Observable> { return room.getEventSummaryLive(eventId).asObservable() } - fun liveTimelineEvent(eventId: String): Observable { + fun liveTimelineEvent(eventId: String): Observable> { return room.getTimeLineEventLive(eventId).asObservable() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 1cf81027f4..c3564c27f8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService +import im.vector.matrix.android.api.util.Optional /** * This interface defines methods to interact within a room. @@ -49,7 +50,7 @@ interface Room : * A live [RoomSummary] associated with the room * You can observe this summary to get dynamic data from this room. */ - fun getRoomSummaryLive(): LiveData + fun getRoomSummaryLive(): LiveData> fun roomSummary(): RoomSummary? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 0c4e1bebc1..5f85671e0d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional /** * In some cases, events may wish to reference other events. @@ -111,7 +112,7 @@ interface RelationService { replyText: String, autoMarkdown: Boolean = false): Cancelable? - fun getEventSummaryLive(eventId: String): LiveData + fun getEventSummaryLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index b55bc17946..b36a8631c3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.timeline import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.util.Optional /** * This interface defines methods to interact with the timeline. It's implemented at the room level. @@ -36,5 +37,5 @@ interface TimelineService { fun getTimeLineEvent(eventId: String): TimelineEvent? - fun getTimeLineEventLive(eventId: String): LiveData + fun getTimeLineEventLive(eventId: String): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt index 19fbe2cc88..6685a6661c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -31,6 +31,10 @@ data class Optional constructor(private val value: T?) { return value ?: fn() } + fun hasValue(): Boolean{ + return value != null + } + companion object { fun from(value: T?): Optional { return Optional(value) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index 949aa6611e..9ae9af8b98 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -39,9 +39,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject +import javax.inject.Provider internal class DefaultAuthenticator @Inject constructor(@Unauthenticated - private val okHttpClient: OkHttpClient, + private val okHttpClient: Provider, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionParamsStore: SessionParamsStore, @@ -119,7 +120,7 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { - val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) + val retrofit = retrofitFactory.create(okHttpClient.get(), homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt deleted file mode 100644 index 46a431a412..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveData.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.database - -import androidx.lifecycle.LiveData -import io.realm.* - -class RealmLiveData(private val realmConfiguration: RealmConfiguration, - private val query: (Realm) -> RealmQuery) : LiveData>() { - - private val listener = RealmChangeListener> { results -> - value = results - } - - private var realm: Realm? = null - private var results: RealmResults? = null - - override fun onActive() { - val realm = Realm.getInstance(realmConfiguration) - val results = query.invoke(realm).findAllAsync() - results.addChangeListener(listener) - this.realm = realm - this.results = results - } - - override fun onInactive() { - results?.removeChangeListener(listener) - results = null - realm?.close() - realm = null - } -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index ac1a828a4f..228552592e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -29,7 +29,8 @@ import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService -import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields @@ -56,19 +57,13 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, RelationService by relationService, MembershipService by roomMembersService { - override fun getRoomSummaryLive(): LiveData { - val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> - RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - } - return Transformations.map(liveRealmData) { results -> - val roomSummaries = results.map { roomSummaryMapper.map(it) } - - if (roomSummaries.isEmpty()) { - // Create a dummy RoomSummary to avoid Crash during Sign Out or clear cache - RoomSummary(roomId) - } else { - roomSummaries.first() - } + override fun getRoomSummaryLive(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, + { roomSummaryMapper.map(it) } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt index c5676f84c8..424598d590 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt @@ -24,7 +24,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.UserDraft -import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.DraftMapper import im.vector.matrix.android.internal.database.model.DraftEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -51,12 +50,13 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private monarchy.writeAsync { realm -> - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) val userDraftsEntity = roomSummaryEntity.userDrafts - ?: realm.createObject().also { - roomSummaryEntity.userDrafts = it - } + ?: realm.createObject().also { + roomSummaryEntity.userDrafts = it + } userDraftsEntity.let { userDraftEntity -> // Save only valid draft @@ -150,16 +150,16 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private } override fun getDraftsLive(): LiveData> { - val liveData = RealmLiveData(monarchy.realmConfiguration) { - UserDraftsEntity.where(it, roomId) - } - - return Transformations.map(liveData) { userDraftsEntities -> - userDraftsEntities.firstOrNull()?.let { userDraftEntity -> - userDraftEntity.userDrafts.map { draftEntity -> - DraftMapper.map(draftEntity) + val liveData = monarchy.findAllMappedWithChanges( + { UserDraftsEntity.where(it, roomId) }, + { + it.userDrafts.map { draft -> + DraftMapper.map(draft) + } } - } ?: emptyList() + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: emptyList() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index c158f09e19..ea15367528 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -25,7 +25,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.util.Optional -import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity @@ -82,33 +82,33 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private } override fun getReadMarkerLive(): LiveData> { - val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> - ReadMarkerEntity.where(realm, roomId) - } - return Transformations.map(liveRealmData) { results -> - Optional.from(results.firstOrNull()?.eventId) + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadMarkerEntity.where(it, roomId) }, + { it.eventId } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().toOptional() } } override fun getMyReadReceiptLive(): LiveData> { - val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> - ReadReceiptEntity.where(realm, roomId = roomId, userId = userId) - } - return Transformations.map(liveRealmData) { results -> - Optional.from(results.firstOrNull()?.eventId) + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) }, + { it.eventId } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull().toOptional() } } override fun getEventReadReceiptsLive(eventId: String): LiveData> { - val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> - ReadReceiptsSummaryEntity.where(realm, eventId) - } - return Transformations.map(liveEntity) { realmResults -> - realmResults.firstOrNull()?.let { - readReceiptsSummaryMapper.map(it) - }?.sortedByDescending { - it.originServerTs - } ?: emptyList() + + val liveRealmData = monarchy.findAllMappedWithChanges( + { ReadReceiptsSummaryEntity.where(it, eventId) }, + { readReceiptsSummaryMapper.map(it) } + ) + return Transformations.map(liveRealmData) { + it.firstOrNull() ?: emptyList() } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index a1abc94f29..9df34c1c50 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -30,7 +30,8 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity @@ -210,13 +211,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } - override fun getEventSummaryLive(eventId: String): LiveData { - val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm -> - EventAnnotationsSummaryEntity.where(realm, eventId) - } - return Transformations.map(liveEntity) { realmResults -> - realmResults.firstOrNull()?.asDomain() - ?: EventAnnotationsSummary(eventId, emptyList(), null) + override fun getEventSummaryLive(eventId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { EventAnnotationsSummaryEntity.where(it, eventId)}, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 1e3bcbc089..8f7d4b9905 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -26,7 +26,8 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineSettings -import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -75,12 +76,13 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv }) } - override fun getTimeLineEventLive(eventId: String): LiveData { - val liveData = RealmLiveData(monarchy.realmConfiguration) { - TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) - } + override fun getTimeLineEventLive(eventId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) }, + { timelineEventMapper.map(it) } + ) return Transformations.map(liveData) { events -> - events.firstOrNull()?.let { timelineEventMapper.map(it) } + events.firstOrNull().toOptional() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 40e79d7735..982fd03cb0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -28,7 +28,6 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional -import im.vector.matrix.android.internal.database.RealmLiveData import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntityFields @@ -63,20 +62,18 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null + ?: return null return userEntity.asDomain() } override fun liveUser(userId: String): LiveData> { - val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> - UserEntity.where(realm, userId) - } - return Transformations.map(liveRealmData) { results -> - results - .map { it.asDomain() } - .firstOrNull() - .toOptional() + val liveData = monarchy.findAllMappedWithChanges( + { UserEntity.where(it, userId) }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() } } diff --git a/vector/build.gradle b/vector/build.gradle index 16bf70aef0..c025219dae 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -234,7 +234,9 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1' implementation 'androidx.core:core-ktx:1.0.2' - implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' + implementation "org.threeten:threetenbp:1.4.0:no-tzdb" + implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" + implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" @@ -280,6 +282,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" implementation 'me.saket:better-link-movement-method:2.2.0' + implementation 'com.google.android:flexbox:1.1.1' // Bus implementation 'org.greenrobot:eventbus:3.1.1' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 1195d38b90..837f63efe9 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -354,8 +354,11 @@ SOFTWARE.
Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org) - - +
  • + LazyThreeTenBp +
    + Copyright 2017 Gabriel Ittner. +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index bf56e71915..4884354ef3 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -31,9 +31,9 @@ import androidx.multidex.MultiDex
     import com.airbnb.epoxy.EpoxyAsyncUtil
     import com.airbnb.epoxy.EpoxyController
     import com.facebook.stetho.Stetho
    +import com.gabrielittner.threetenbp.LazyThreeTen
     import com.github.piasy.biv.BigImageViewer
     import com.github.piasy.biv.loader.glide.GlideImageLoader
    -import com.jakewharton.threetenabp.AndroidThreeTen
     import im.vector.matrix.android.api.Matrix
     import im.vector.matrix.android.api.MatrixConfiguration
     import im.vector.matrix.android.api.auth.Authenticator
    @@ -96,7 +96,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
                 Stetho.initializeWithDefaults(this)
             }
             logInfo()
    -        AndroidThreeTen.init(this)
    +        LazyThreeTen.init(this)
    +
             BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
             EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
             EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt
    index 2615643781..df45bbc9a0 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt
    @@ -70,7 +70,7 @@ class GroupListFragment : VectorBaseFragment(), GroupSummaryController.Callback
                 is Incomplete -> stateView.state = StateView.State.Loading
                 is Success    -> stateView.state = StateView.State.Content
             }
    -        groupController.setData(state)
    +        groupController.update(state)
         }
     
         override fun onGroupSelected(groupSummary: GroupSummary) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt
    index 785f833077..91fb02a74e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt
    @@ -16,17 +16,29 @@
     
     package im.vector.riotx.features.home.group
     
    +import com.airbnb.epoxy.EpoxyController
     import com.airbnb.epoxy.TypedEpoxyController
     import im.vector.matrix.android.api.session.group.model.GroupSummary
     import im.vector.riotx.features.home.AvatarRenderer
     import javax.inject.Inject
     
    -class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer): TypedEpoxyController() {
    +class GroupSummaryController @Inject constructor(private val avatarRenderer: AvatarRenderer) : EpoxyController() {
     
         var callback: Callback? = null
    +    private var viewState: GroupListViewState? = null
     
    -    override fun buildModels(viewState: GroupListViewState) {
    -        buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
    +    init {
    +        requestModelBuild()
    +    }
    +
    +    fun update(viewState: GroupListViewState) {
    +        this.viewState = viewState
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val nonNullViewState = viewState ?: return
    +        buildGroupModels(nonNullViewState.asyncGroups(), nonNullViewState.selectedGroup)
         }
     
         private fun buildGroupModels(summaries: List?, selected: GroupSummary?) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index a4623a5b23..4b8e46b0d9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -50,6 +50,7 @@ import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
     import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
     import im.vector.matrix.rx.rx
    +import im.vector.matrix.rx.unwrap
     import im.vector.riotx.BuildConfig
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.postLiveEvent
    @@ -719,6 +720,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     
         private fun observeRoomSummary() {
             room.rx().liveRoomSummary()
    +                .unwrap()
                     .execute { async ->
                         copy(
                                 asyncRoomSummary = async,
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index 9f61f014e6..c1a8ea8bcc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
     import im.vector.matrix.rx.RxRoom
    +import im.vector.matrix.rx.unwrap
     import im.vector.riotx.core.extensions.canReact
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
    @@ -51,7 +52,7 @@ data class MessageActionState(
         fun senderName(): String = informationData.memberName?.toString() ?: ""
     
         fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
    -            ?: ""
    +                          ?: ""
     
         fun canReact() = timelineEvent()?.canReact() == true
     
    @@ -61,7 +62,7 @@ data class MessageActionState(
                     val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
                     if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
                         eventHtmlRenderer?.render(messageContent.formattedBody
    -                            ?: messageContent.body)
    +                                              ?: messageContent.body)
                     } else {
                         messageContent?.body
                     }
    @@ -116,6 +117,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             if (room == null) return
             RxRoom(room)
                     .liveTimelineEvent(eventId)
    +                .unwrap()
                     .execute {
                         copy(timelineEvent = it)
                     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
    index d89035b813..a39ad0feca 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
    @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
     import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
    +import im.vector.matrix.android.api.util.Optional
     import im.vector.matrix.rx.RxRoom
     import im.vector.riotx.R
     import im.vector.riotx.core.extensions.canReact
    @@ -110,7 +111,9 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
                     }
         }
     
    -    private fun actionsForEvent(event: TimelineEvent): List {
    +    private fun actionsForEvent(optionalEvent: Optional): List {
    +        val event = optionalEvent.getOrNull() ?: return emptyList()
    +
             val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
                     ?: event.root.getClearContent().toModel()
             val type = messageContent?.type
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt
    index d2a86f43a0..290bb5427b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/QuickReactionViewModel.kt
    @@ -77,7 +77,7 @@ class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState:
                     .liveAnnotationSummary(eventId)
                     .map { annotations ->
                         quickEmojis.map { emoji ->
    -                        ToggleState(emoji, annotations.reactionsSummary.firstOrNull { it.key == emoji }?.addedByMe
    +                        ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe
                                     ?: false)
                         }
                     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt
    index 221cc889f0..2f5b561d20 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewReactionViewModel.kt
    @@ -27,6 +27,7 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
     import im.vector.matrix.rx.RxRoom
    +import im.vector.matrix.rx.unwrap
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.utils.isSingleEmoji
     import im.vector.riotx.core.date.VectorDateFormatter
    @@ -87,6 +88,7 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
         private fun observeEventAnnotationSummaries() {
             RxRoom(room)
                     .liveAnnotationSummary(eventId)
    +                .unwrap()
                     .flatMapSingle { summaries ->
                         Observable
                                 .fromIterable(summaries.reactionsSummary)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    index 91b60c6fbe..9e67ffb590 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    @@ -250,6 +250,7 @@ class MessageItemFactory @Inject constructor(
                                          callback: TimelineEventController.Callback?,
                                          attributes: AbsMessageItem.Attributes): MessageTextItem? {
     
    +        val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
             val bodyToUse = messageContent.formattedBody?.let {
                 htmlRenderer.get().render(it.trim())
             } ?: messageContent.body
    @@ -265,6 +266,7 @@ class MessageItemFactory @Inject constructor(
                             message(linkifiedBody)
                         }
                     }
    +                .searchForPills(isFormatted)
                     .leftGuideline(avatarSizeProvider.leftGuideline)
                     .attributes(attributes)
                     .highlighted(highlight)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
    index a4bb5c88cd..be44026166 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
    @@ -17,16 +17,11 @@
     package im.vector.riotx.features.home.room.detail.timeline.item
     
     import android.graphics.Typeface
    -import android.os.Build
     import android.view.View
     import android.view.ViewGroup
    -import android.view.ViewStub
     import android.widget.ImageView
     import android.widget.TextView
     import androidx.annotation.IdRes
    -import androidx.constraintlayout.helper.widget.Flow
    -import androidx.core.view.children
    -import androidx.core.view.isGone
     import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import im.vector.matrix.android.api.session.room.send.SendState
    @@ -122,38 +117,23 @@ abstract class AbsMessageItem : BaseEventItem() {
                     _readMarkerCallback
             )
     
    -        if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
    -            holder.reactionWrapper?.isVisible = false
    -
    +        val reactions = attributes.informationData.orderedReactionList
    +        if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
    +            holder.reactionsContainer.isVisible = false
             } else {
    -            //inflate if needed
    -            if (holder.reactionFlowHelper == null) {
    -                holder.reactionWrapper = holder.view.findViewById(R.id.messageBottomInfo).inflate() as? ViewGroup
    -                holder.reactionFlowHelper = holder.view.findViewById(R.id.reactionsFlowHelper)
    +            holder.reactionsContainer.isVisible = true
    +            holder.reactionsContainer.removeAllViews()
    +            reactions.take(8).forEach { reaction ->
    +                val reactionButton = ReactionButton(holder.view.context)
    +                reactionButton.reactedListener = reactionClickListener
    +                reactionButton.setTag(R.id.reactionsContainer, reaction.key)
    +                reactionButton.reactionString = reaction.key
    +                reactionButton.reactionCount = reaction.count
    +                reactionButton.setChecked(reaction.addedByMe)
    +                reactionButton.isEnabled = reaction.synced
    +                holder.reactionsContainer.addView(reactionButton)
                 }
    -            holder.reactionWrapper?.isVisible = true
    -            //clear all reaction buttons (but not the Flow helper!)
    -            holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
    -            val idToRefInFlow = ArrayList()
    -            attributes.informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction ->
    -                (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
    -                    reactionButton.isVisible = true
    -                    reactionButton.reactedListener = reactionClickListener
    -                    reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
    -                    idToRefInFlow.add(reactionButton.id)
    -                    reactionButton.reactionString = reaction.key
    -                    reactionButton.reactionCount = reaction.count
    -                    reactionButton.setChecked(reaction.addedByMe)
    -                    reactionButton.isEnabled = reaction.synced
    -                }
    -            }
    -            // Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
    -            // so have to update ref ids
    -            holder.reactionFlowHelper?.referencedIds = idToRefInFlow.toIntArray()
    -            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
    -                holder.reactionFlowHelper?.requestLayout()
    -            }
    -            holder.reactionWrapper?.setOnLongClickListener(attributes.itemLongClickListener)
    +            holder.reactionsContainer.setOnLongClickListener(attributes.itemLongClickListener)
             }
         }
     
    @@ -181,8 +161,7 @@ abstract class AbsMessageItem : BaseEventItem() {
             val avatarImageView by bind(R.id.messageAvatarImageView)
             val memberNameView by bind(R.id.messageMemberNameView)
             val timeView by bind(R.id.messageTimeView)
    -        var reactionWrapper: ViewGroup? = null
    -        var reactionFlowHelper: Flow? = null
    +        val reactionsContainer by bind(R.id.reactionsContainer)
         }
     
         /**
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
    index c6e813e878..8ac8264cad 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt
    @@ -17,8 +17,11 @@ package im.vector.riotx.features.home.room.detail.timeline.item
     
     import android.view.View
     import android.view.ViewStub
    +import android.widget.RelativeLayout
     import androidx.annotation.IdRes
     import androidx.constraintlayout.widget.Guideline
    +import androidx.core.view.marginStart
    +import androidx.core.view.updateLayoutParams
     import com.airbnb.epoxy.EpoxyAttribute
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    @@ -44,7 +47,9 @@ abstract class BaseEventItem : VectorEpoxyModel
     
         override fun bind(holder: H) {
             super.bind(holder)
    -        holder.leftGuideline.setGuidelineBegin(leftGuideline)
    +        holder.leftGuideline.updateLayoutParams {
    +            this.marginStart = leftGuideline
    +        }
             holder.checkableBackground.isChecked = highlighted
         }
     
    @@ -55,7 +60,7 @@ abstract class BaseEventItem : VectorEpoxyModel
         abstract fun getEventIds(): List
     
         abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
    -        val leftGuideline by bind(R.id.messageStartGuideline)
    +        val leftGuideline by bind(R.id.messageStartGuideline)
             val checkableBackground by bind(R.id.messageSelectedBackground)
             val readReceiptsView by bind(R.id.readReceiptsView)
             val readMarkerView by bind(R.id.readMarkerView)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
    index 2ba2337242..a19a335937 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageTextItem.kt
    @@ -35,6 +35,8 @@ import me.saket.bettermovementmethod.BetterLinkMovementMethod
     @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
     abstract class MessageTextItem : AbsMessageItem() {
     
    +    @EpoxyAttribute
    +    var searchForPills: Boolean = false
         @EpoxyAttribute
         var message: CharSequence? = null
         @EpoxyAttribute
    @@ -65,22 +67,22 @@ abstract class MessageTextItem : AbsMessageItem() {
         override fun bind(holder: Holder) {
             super.bind(holder)
             holder.messageView.movementMethod = mvmtMethod
    -
             if (useBigFont) {
                 holder.messageView.textSize = 44F
             } else {
                 holder.messageView.textSize = 14F
             }
    -
    -        val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
    -                TextViewCompat.getTextMetricsParams(holder.messageView),
    -                null)
    -
    -        holder.messageView.setTextFuture(textFuture)
             renderSendState(holder.messageView, holder.messageView)
             holder.messageView.setOnClickListener(attributes.itemClickListener)
             holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
    -        findPillsAndProcess { it.bind(holder.messageView) }
    +        if (searchForPills) {
    +            findPillsAndProcess { it.bind(holder.messageView) }
    +        }
    +        val textFuture = PrecomputedTextCompat.getTextFuture(
    +                message ?: "",
    +                TextViewCompat.getTextMetricsParams(holder.messageView),
    +                null)
    +        holder.messageView.setTextFuture(textFuture)
         }
     
         private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    index afe3579d76..cc2224b64a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt
    @@ -41,6 +41,7 @@ import im.vector.riotx.features.home.room.list.widget.FabMenuView
     import im.vector.riotx.features.notifications.NotificationDrawerManager
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_room_list.*
    +import timber.log.Timber
     import javax.inject.Inject
     
     @Parcelize
    @@ -180,7 +181,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
                 is Success    -> renderSuccess(state)
                 is Fail       -> renderFailure(state.asyncFilteredRooms.error)
             }
    -        roomController.setData(state)
    +        roomController.update(state)
         }
     
         private fun renderSuccess(state: RoomListViewState) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    index 42e3a3db85..daee3876aa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt
    @@ -17,40 +17,56 @@
     package im.vector.riotx.features.home.room.list
     
     import androidx.annotation.StringRes
    +import com.airbnb.epoxy.EpoxyController
     import com.airbnb.epoxy.TypedEpoxyController
     import im.vector.matrix.android.api.session.room.model.Membership
     import im.vector.matrix.android.api.session.room.model.RoomSummary
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem
     import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem
    +import timber.log.Timber
     import javax.inject.Inject
     
     class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider,
                                                     private val roomSummaryItemFactory: RoomSummaryItemFactory,
                                                     private val roomListNameFilter: RoomListNameFilter
    -) : TypedEpoxyController() {
    +) : EpoxyController() {
     
         var listener: Listener? = null
     
    -    override fun buildModels(viewState: RoomListViewState) {
    -        if (viewState.displayMode == RoomListFragment.DisplayMode.FILTERED) {
    -            buildFilteredRooms(viewState)
    +    private var viewState: RoomListViewState? = null
    +
    +    init {
    +        // We are requesting a model build directly as the first build of epoxy is on the main thread.
    +        // It avoids to build the the whole list of rooms on the main thread.
    +        requestModelBuild()
    +    }
    +
    +    fun update(viewState: RoomListViewState) {
    +        this.viewState = viewState
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val nonNullViewState = viewState ?: return
    +        if (nonNullViewState.displayMode == RoomListFragment.DisplayMode.FILTERED) {
    +            buildFilteredRooms(nonNullViewState)
             } else {
    -            val roomSummaries = viewState.asyncFilteredRooms()
    +            val roomSummaries = nonNullViewState.asyncFilteredRooms()
                 roomSummaries?.forEach { (category, summaries) ->
                     if (summaries.isEmpty()) {
                         return@forEach
                     } else {
    -                    val isExpanded = viewState.isCategoryExpanded(category)
    -                    buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
    +                    val isExpanded = nonNullViewState.isCategoryExpanded(category)
    +                    buildRoomCategory(nonNullViewState, summaries, category.titleRes, nonNullViewState.isCategoryExpanded(category)) {
                             listener?.onToggleRoomCategory(category)
                         }
                         if (isExpanded) {
                             buildRoomModels(summaries,
    -                                viewState.joiningRoomsIds,
    -                                viewState.joiningErrorRoomsIds,
    -                                viewState.rejectingRoomsIds,
    -                                viewState.rejectingErrorRoomsIds)
    +                                        nonNullViewState.joiningRoomsIds,
    +                                        nonNullViewState.joiningErrorRoomsIds,
    +                                        nonNullViewState.rejectingRoomsIds,
    +                                        nonNullViewState.rejectingErrorRoomsIds)
                         }
                     }
                 }
    @@ -66,10 +82,10 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                     .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
     
             buildRoomModels(filteredSummaries,
    -                viewState.joiningRoomsIds,
    -                viewState.joiningErrorRoomsIds,
    -                viewState.rejectingRoomsIds,
    -                viewState.rejectingErrorRoomsIds)
    +                        viewState.joiningRoomsIds,
    +                        viewState.joiningErrorRoomsIds,
    +                        viewState.rejectingRoomsIds,
    +                        viewState.rejectingErrorRoomsIds)
     
             addFilterFooter(viewState)
         }
    @@ -105,7 +121,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
                 showHighlighted(showHighlighted)
                 listener {
                     mutateExpandedState()
    -                setData(viewState)
    +                update(viewState)
                 }
             }
         }
    diff --git a/vector/src/main/res/drawable/reaction_divider.xml b/vector/src/main/res/drawable/reaction_divider.xml
    new file mode 100644
    index 0000000000..d68b6a9094
    --- /dev/null
    +++ b/vector/src/main/res/drawable/reaction_divider.xml
    @@ -0,0 +1,8 @@
    +
    +
    +    
    +    
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml
    index 4eb9be0b9f..ec5cac245d 100644
    --- a/vector/src/main/res/layout/item_timeline_event_base.xml
    +++ b/vector/src/main/res/layout/item_timeline_event_base.xml
    @@ -1,5 +1,5 @@
     
    -
    +        android:layout_width="match_parent"
    +        android:layout_height="match_parent"
    +        android:layout_alignBottom="@+id/readMarkerView"
    +        android:layout_alignParentTop="true"
    +        android:background="?riotx_highlighted_message_background" />
     
         
     
    -    
    -
         
     
    -
         
     
    -
    -    
    -
    -    
    -
    -    
    -
    -    
    -
    -
    -    
    -    
    +        android:layout_height="0dp"
    +        tools:layout_marginStart="52dp" />
     
    -    
    -
    -    
    +        android:layout_below="@id/messageMemberNameView"
    +        android:layout_toEndOf="@id/messageStartGuideline"
    +        android:addStatesFromChildren="true">
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +    
    +
    +    
    +
    +        
    +
    +        
    +
    +    
     
         
    +        android:layout_marginEnd="8dp"
    +        android:layout_marginBottom="2dp"
    +        android:background="?attr/vctr_unread_marker_line_color"
    +        android:visibility="invisible" />
     
    -
    \ No newline at end of file
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml
    index fc4a527d03..583997577a 100644
    --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml
    +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml
    @@ -1,6 +1,5 @@
     
    -
    +        android:layout_width="match_parent"
    +        android:layout_height="match_parent"
    +        android:layout_alignBottom="@+id/informationBottom"
    +        android:layout_alignParentTop="true"
    +        android:background="?riotx_highlighted_message_background" />
     
    -    
    -
    -    
    -
    -    
    -
    -    
    +        android:layout_marginStart="52dp" />
     
    -    
    -
    -    
    +        android:layout_alignParentTop="true"
    +        android:layout_toEndOf="@id/messageStartGuideline">
     
    -    
    +        
     
    +        
     
    -
    \ No newline at end of file
    +        
    +
    +        
    +
    +    
    +
    +    
    +
    +        
    +
    +        
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_timeline_event_bottom_reactions_stub.xml b/vector/src/main/res/layout/item_timeline_event_bottom_reactions_stub.xml
    deleted file mode 100644
    index fb03135212..0000000000
    --- a/vector/src/main/res/layout/item_timeline_event_bottom_reactions_stub.xml
    +++ /dev/null
    @@ -1,102 +0,0 @@
    -
    -
    -
    -    
    -
    -    
    -
    -    
    -
    -    
    -
    -    
    -
    -
    -    
    -
    -
    -    
    -
    -    
    -
    -
    -    
    -
    -
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml
    index 46c84aa4e7..ede5b0e749 100644
    --- a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml
    +++ b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml
    @@ -1,61 +1,58 @@
     
     
    -
    +    android:layout_height="wrap_content"
    +    android:orientation="vertical">
     
    -    
    +    
     
    -    
    +        
    +
    +        
    +
    +    
     
         
    +        android:background="?attr/riotx_header_panel_background"/>
    +
     
         
     
    -
    \ No newline at end of file
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml
    index d1894254cd..1b19c87e08 100644
    --- a/vector/src/main/res/values/styles_riot.xml
    +++ b/vector/src/main/res/values/styles_riot.xml
    @@ -282,7 +282,7 @@
     
     
         
    -
    -    
    -
    -
    -