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 e5ebc536ff..bbf0e76823 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 @@ -17,13 +17,16 @@ package im.vector.matrix.rx import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import io.reactivex.Observable import io.reactivex.Single @@ -31,18 +34,22 @@ class RxRoom(private val room: Room) { fun liveRoomSummary(): Observable> { return room.getRoomSummaryLive().asObservable() + .startWith(room.roomSummary().toOptional()) } - fun liveRoomMemberIds(): Observable> { - return room.getRoomMemberIdsLive().asObservable() + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { + return room.getRoomMembersLive(queryParams).asObservable() + .startWith(room.getRoomMembers(queryParams)) } fun liveAnnotationSummary(eventId: String): Observable> { - return room.getEventSummaryLive(eventId).asObservable() + return room.getEventAnnotationsSummaryLive(eventId).asObservable() + .startWith(room.getEventAnnotationsSummary(eventId).toOptional()) } fun liveTimelineEvent(eventId: String): Observable> { return room.getTimeLineEventLive(eventId).asObservable() + .startWith(room.getTimeLineEvent(eventId).toOptional()) } fun liveReadMarker(): Observable> { diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index c9381b861d..084f497de5 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -18,8 +18,10 @@ package im.vector.matrix.rx import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState @@ -30,40 +32,43 @@ import io.reactivex.Single class RxSession(private val session: Session) { - fun liveRoomSummaries(): Observable> { - return session.liveRoomSummaries().asObservable() + fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> { + return session.getRoomSummariesLive(queryParams).asObservable() + .startWith(session.getRoomSummaries(queryParams)) } - fun liveGroupSummaries(): Observable> { - return session.liveGroupSummaries().asObservable() + fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> { + return session.getGroupSummariesLive(queryParams).asObservable() + .startWith(session.getGroupSummaries(queryParams)) } fun liveBreadcrumbs(): Observable> { - return session.liveBreadcrumbs().asObservable() + return session.getBreadcrumbsLive().asObservable() + .startWith(session.getBreadcrumbs()) } fun liveSyncState(): Observable { - return session.syncState().asObservable() + return session.getSyncStateLive().asObservable() } fun livePushers(): Observable> { - return session.livePushers().asObservable() + return session.getPushersLive().asObservable() } fun liveUser(userId: String): Observable> { - return session.liveUser(userId).asObservable().distinctUntilChanged() + return session.getUserLive(userId).asObservable().distinctUntilChanged() } fun liveUsers(): Observable> { - return session.liveUsers().asObservable() + return session.getUsersLive().asObservable() } fun liveIgnoredUsers(): Observable> { - return session.liveIgnoredUsers().asObservable() + return session.getIgnoredUsersLive().asObservable() } fun livePagedUsers(filter: String? = null): Observable> { - return session.livePagedUsers(filter).asObservable() + return session.getPagedUsersLive(filter).asObservable() } fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ab5f122dbc..7a1348a54c 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -10,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath "io.realm:realm-gradle-plugin:5.12.0" + classpath "io.realm:realm-gradle-plugin:6.0.2" } } @@ -102,7 +102,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0-beta05" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" @@ -119,14 +118,14 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.0.0' + implementation 'androidx.exifinterface:exifinterface:1.1.0' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" + implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index 592086b0ec..3980094175 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -19,7 +19,10 @@ package im.vector.matrix.android.session.room.timeline import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.internal.database.helper.* +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.internal.database.helper.lastStateIndex +import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection @@ -29,7 +32,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject -import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldEqual import org.junit.Before @@ -146,30 +148,6 @@ internal class ChunkEntityTest : InstrumentedTest { } } - @Test - fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { - monarchy.runTransactionSync { realm -> - val chunk1: ChunkEntity = realm.createObject() - val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked().shouldBeFalse() - } - } - - @Test - fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() { - monarchy.runTransactionSync { realm -> - val chunk1: ChunkEntity = realm.createObject() - val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) - chunk1.isUnlinked().shouldBeTrue() - } - } - @Test fun merge_shouldPrevTokenMerged_whenMergingForwards() { monarchy.runTransactionSync { realm -> @@ -177,8 +155,8 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val prevToken = "prev_token" chunk1.prevToken = prevToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.prevToken shouldEqual prevToken } @@ -191,10 +169,19 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val nextToken = "next_token" chunk1.nextToken = nextToken - chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.nextToken shouldEqual nextToken } } + + private fun ChunkEntity.addAll(roomId: String, + events: List, + direction: PaginationDirection, + stateIndexOffset: Int = 0) { + events.forEach { event -> + add(roomId, event, direction, stateIndexOffset) + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 8a8ee11854..dd4daee9cd 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.session.room.timeline -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType @@ -25,12 +24,6 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import io.realm.kotlin.createObject import kotlin.random.Random object RoomDataHelper { @@ -73,19 +66,4 @@ object RoomDataHelper { val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) } - - fun fakeInitialSync(monarchy: Monarchy, roomId: String) { - monarchy.runTransactionSync { realm -> - val roomEntity = realm.createObject(roomId) - roomEntity.membership = Membership.JOIN - val eventList = createFakeListOfEvents(10) - val chunkEntity = realm.createObject().apply { - nextToken = null - prevToken = Random.nextLong(System.currentTimeMillis()).toString() - isLastForward = true - } - chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS) - roomEntity.addOrUpdate(chunkEntity) - } - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt new file mode 100644 index 0000000000..5d3e76f1d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/query/QueryStringValue.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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.api.query + +/** + * Basic query language. All these cases are mutually exclusive. + */ +sealed class QueryStringValue { + object NoCondition : QueryStringValue() + object IsNull : QueryStringValue() + object IsNotNull : QueryStringValue() + object IsEmpty : QueryStringValue() + object IsNotEmpty : QueryStringValue() + data class Equals(val string: String, val case: Case) : QueryStringValue() + data class Contains(val string: String, val case: Case) : QueryStringValue() + + enum class Case { + SENSITIVE, + INSENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index ab545dbce6..1c73d4c5d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -107,7 +107,7 @@ interface Session : * This method allows to listen the sync state. * @return a [LiveData] of [SyncState]. */ - fun syncState(): LiveData + fun getSyncStateLive(): LiveData /** * This methods return true if an initial sync has been processed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt index 2d55d0be57..c01e5b5cd8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupService.kt @@ -38,9 +38,15 @@ interface GroupService { */ fun getGroupSummary(groupId: String): GroupSummary? + /** + * Get a list of group summaries. This list is a snapshot of the data. + * @return the list of [GroupSummary] + */ + fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List + /** * Get a live list of group summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of [GroupSummary] */ - fun liveGroupSummaries(): LiveData> + fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt new file mode 100644 index 0000000000..702b8c2523 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/group/GroupSummaryQueryParams.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.api.session.group + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams { + return GroupSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter group summaries + */ +data class GroupSummaryQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = GroupSummaryQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt index d082faa7c7..129bfa3011 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt @@ -58,7 +58,7 @@ interface PushersService { const val EVENT_ID_ONLY = "event_id_only" } - fun livePushers(): LiveData> + fun getPushersLive(): LiveData> fun pushers() : List } 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 90790a6ab0..3221c355e8 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 @@ -56,5 +56,8 @@ interface Room : */ fun getRoomSummaryLive(): LiveData> + /** + * A current snapshot of [RoomSummary] associated with the room + */ fun roomSummary(): RoomSummary? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index ba3b5ded78..f3167c8461 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -60,16 +60,28 @@ interface RoomService { fun getRoomSummary(roomIdOrAlias: String): RoomSummary? /** - * Get a live list of room summaries. This list is refreshed as soon as the data changes. - * @return the [LiveData] of [RoomSummary] + * Get a snapshot list of room summaries. + * @return the immutable list of [RoomSummary] */ - fun liveRoomSummaries(): LiveData> + fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List + + /** + * Get a live list of room summaries. This list is refreshed as soon as the data changes. + * @return the [LiveData] of List[RoomSummary] + */ + fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> + + /** + * Get a snapshot list of Breadcrumbs + * @return the immutable list of [RoomSummary] + */ + fun getBreadcrumbs(): List /** * Get a live list of Breadcrumbs * @return the [LiveData] of [RoomSummary] */ - fun liveBreadcrumbs(): LiveData> + fun getBreadcrumbsLive(): LiveData> /** * Inform the Matrix SDK that a room is displayed. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt new file mode 100644 index 0000000000..6983bda225 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomSummaryQueryParams.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2020 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.api.session.room + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { + return RoomSummaryQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room summaries to use with: + * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] + */ +data class RoomSummaryQueryParams( + val displayName: QueryStringValue, + val canonicalAlias: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition + var memberships: List = Membership.all() + + fun build() = RoomSummaryQueryParams( + displayName = displayName, + canonicalAlias = canonicalAlias, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 34af2cf572..6c117d3be7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -41,11 +41,18 @@ interface MembershipService { fun getRoomMember(userId: String): RoomMember? /** - * Return all the roomMembers ids of the room - * + * Return all the roomMembers of the room with params + * @param queryParams the params to query for + * @return a roomMember list. + */ + fun getRoomMembers(queryParams: RoomMemberQueryParams): List + + /** + * Return all the roomMembers of the room filtered by memberships + * @param queryParams the params to query for * @return a [LiveData] of roomMember list. */ - fun getRoomMemberIdsLive(): LiveData> + fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> fun getNumberOfJoinedMembers(): Int diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt new file mode 100644 index 0000000000..19003632ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.api.session.room.members + +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.room.model.Membership + +fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams { + return RoomMemberQueryParams.Builder().apply(init).build() +} + +/** + * This class can be used to filter room members + */ +data class RoomMemberQueryParams( + val displayName: QueryStringValue, + val memberships: List +) { + + class Builder { + + var displayName: QueryStringValue = QueryStringValue.IsNotEmpty + var memberships: List = Membership.all() + + fun build() = RoomMemberQueryParams( + displayName = displayName, + memberships = memberships + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 1894effc7a..7c6a931373 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -43,4 +43,14 @@ enum class Membership(val value: String) { fun isLeft(): Boolean { return this == KNOCK || this == LEAVE || this == BAN } + + companion object { + fun activeMemberships(): List { + return listOf(INVITE, JOIN) + } + + fun all(): List { + return values().asList() + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt index 6a4d8e3c94..994c27be4d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt @@ -16,23 +16,12 @@ package im.vector.matrix.android.api.session.room.model -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.events.model.UnsignedData - /** - * Class representing the EventType.STATE_ROOM_MEMBER state event content + * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content */ -@JsonClass(generateAdapter = true) data class RoomMember( - @Json(name = "membership") val membership: Membership, - @Json(name = "reason") val reason: String? = null, - @Json(name = "displayname") val displayName: String? = null, - @Json(name = "avatar_url") val avatarUrl: String? = null, - @Json(name = "is_direct") val isDirect: Boolean = false, - @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, - @Json(name = "unsigned") val unsignedData: UnsignedData? = null -) { - val safeReason - get() = reason?.takeIf { it.isNotBlank() } -} + val membership: Membership, + val userId: String, + val displayName: String? = null, + val avatarUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt new file mode 100644 index 0000000000..deeeb8ba52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberContent.kt @@ -0,0 +1,38 @@ +/* + * 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.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.UnsignedData + +/** + * Class representing the EventType.STATE_ROOM_MEMBER state event content + */ +@JsonClass(generateAdapter = true) +data class RoomMemberContent( + @Json(name = "membership") val membership: Membership, + @Json(name = "reason") val reason: String? = null, + @Json(name = "displayname") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null, + @Json(name = "is_direct") val isDirect: Boolean = false, + @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, + @Json(name = "unsigned") val unsignedData: UnsignedData? = null +) { + val safeReason + get() = reason?.takeIf { it.isNotBlank() } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 129c35a17e..c18645ddbd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -41,7 +41,8 @@ data class RoomSummary( val membership: Membership = Membership.NONE, val versioningState: VersioningState = VersioningState.NONE, val readMarkerId: String? = null, - val userDrafts: List = emptyList() + val userDrafts: List = emptyList(), + var isEncrypted: Boolean ) { val isVersioned: Boolean 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 7d8f2f0bc1..31ed4e9986 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 @@ -108,5 +108,17 @@ interface RelationService { replyText: CharSequence, autoMarkdown: Boolean = false): Cancelable? - fun getEventSummaryLive(eventId: String): LiveData> + /** + * Get the current EventAnnotationsSummary + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the EventAnnotationsSummary found + */ + fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? + + /** + * Get a LiveData of EventAnnotationsSummary for the specified eventId + * @param eventId the eventId to look for EventAnnotationsSummary + * @return the LiveData of EventAnnotationsSummary + */ + fun getEventAnnotationsSummaryLive(eventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 2a93a876f6..453400bc99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -50,25 +50,25 @@ interface UserService { * @param userId the userId to look for. * @return a LiveData of user with userId */ - fun liveUser(userId: String): LiveData> + fun getUserLive(userId: String): LiveData> /** * Observe a live list of users sorted alphabetically * @return a Livedata of users */ - fun liveUsers(): LiveData> + fun getUsersLive(): LiveData> /** * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * @param filter the filter. It will look into userId and displayName. * @return a Livedata of users */ - fun livePagedUsers(filter: String? = null): LiveData> + fun getPagedUsersLive(filter: String? = null): LiveData> /** * Get list of ignored users */ - fun liveIgnoredUsers(): LiveData> + fun getIgnoredUsersLive(): LiveData> /** * Ignore users diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 4c8082b77e..d6ef522f41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.user.model.User @@ -146,3 +147,4 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) +fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index b9d95035d2..ddc7f5e8e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -59,6 +59,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val .apply() val realmConfiguration = RealmConfiguration.Builder() + .compactOnLaunch() .directory(directory) .name(REALM_NAME) .apply { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index f05fa01444..3fa355fe3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -28,15 +28,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import io.realm.Sort - -// By default if a chunk is empty we consider it unlinked -internal fun ChunkEntity.isUnlinked(): Boolean { - assertIsManaged() - return timelineEvents.where() - .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false) - .findAll() - .isEmpty() -} +import io.realm.kotlin.createObject internal fun ChunkEntity.deleteOnCascade() { assertIsManaged() @@ -46,11 +38,10 @@ internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, - direction: PaginationDirection) { + direction: PaginationDirection): List { assertIsManaged() - val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() - val isCurrentChunkUnlinked = this.isUnlinked() - val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked + val isChunkToMergeUnlinked = chunkToMerge.isUnlinked + val isCurrentChunkUnlinked = isUnlinked if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) { this.timelineEvents.forEach { it.root?.isUnlinked = false } @@ -65,49 +56,21 @@ internal fun ChunkEntity.merge(roomId: String, this.isLastBackward = chunkToMerge.isLastBackward eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) } - val events = eventsToMerge.mapNotNull { it.root?.asDomain() } - val eventIds = ArrayList() - events.forEach { event -> - add(roomId, event, direction, isUnlinked = isUnlinked) - if (event.eventId != null) { - eventIds.add(event.eventId) - } - } - updateSenderDataFor(eventIds) -} - -internal fun ChunkEntity.addAll(roomId: String, - events: List, - direction: PaginationDirection, - stateIndexOffset: Int = 0, - // Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk) - isUnlinked: Boolean = false) { - assertIsManaged() - val eventIds = ArrayList() - events.forEach { event -> - add(roomId, event, direction, stateIndexOffset, isUnlinked) - if (event.eventId != null) { - eventIds.add(event.eventId) - } - } - updateSenderDataFor(eventIds) -} - -internal fun ChunkEntity.updateSenderDataFor(eventIds: List) { - for (eventId in eventIds) { - val timelineEventEntity = timelineEvents.find(eventId) ?: continue - timelineEventEntity.updateSenderData() - } + return eventsToMerge + .mapNotNull { + val event = it.root?.asDomain() ?: return@mapNotNull null + add(roomId, event, direction) + } } internal fun ChunkEntity.add(roomId: String, event: Event, direction: PaginationDirection, - stateIndexOffset: Int = 0, - isUnlinked: Boolean = false) { + stateIndexOffset: Int = 0 +): TimelineEventEntity? { assertIsManaged() if (event.eventId != null && timelineEvents.find(event.eventId) != null) { - return + return null } var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { @@ -129,12 +92,15 @@ internal fun ChunkEntity.add(roomId: String, } } + val isChunkUnlinked = isUnlinked val localId = TimelineEventEntity.nextId(realm) val eventId = event.eventId ?: "" val senderId = event.senderId ?: "" val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() - ?: ReadReceiptsSummaryEntity(eventId, roomId) + ?: realm.createObject(eventId).apply { + this.roomId = roomId + } // Update RR for the sender of a new message with a dummy one @@ -151,13 +117,15 @@ internal fun ChunkEntity.add(roomId: String, } } - val eventEntity = TimelineEventEntity(localId).also { - it.root = event.toEntity(roomId).apply { - this.stateIndex = currentStateIndex - this.isUnlinked = isUnlinked - this.displayIndex = currentDisplayIndex - this.sendState = SendState.SYNCED - } + val rootEvent = event.toEntity(roomId).apply { + this.stateIndex = currentStateIndex + this.displayIndex = currentDisplayIndex + this.sendState = SendState.SYNCED + this.isUnlinked = isChunkUnlinked + } + val eventEntity = realm.createObject().also { + it.localId = localId + it.root = realm.copyToRealm(rootEvent) it.eventId = eventId it.roomId = roomId it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() @@ -165,6 +133,7 @@ internal fun ChunkEntity.add(roomId: String, } val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size timelineEvents.add(position, eventEntity) + return eventEntity } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 948af2af96..19c4715faa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -60,7 +60,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) { this.sendState = SendState.UNSENT } val roomMembers = RoomMembers(realm, roomId) - val myUser = roomMembers.get(senderId) + val myUser = roomMembers.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) val timelineEventEntity = TimelineEventEntity(localId).also { it.root = eventEntity @@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) { it.senderName = myUser?.displayName it.senderAvatar = myUser?.avatarUrl it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName) - it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst() } sendingTimelineEvents.add(0, timelineEventEntity) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt index 36ed2f7edf..0bf02aa92f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventEntityHelper.kt @@ -16,74 +16,9 @@ package im.vector.matrix.android.internal.database.helper -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.model.* -import im.vector.matrix.android.internal.database.query.next -import im.vector.matrix.android.internal.database.query.prev -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.extensions.assertIsManaged -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import io.realm.Realm -import io.realm.RealmList -import io.realm.RealmQuery - -internal fun TimelineEventEntity.updateSenderData() { - assertIsManaged() - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return - val stateIndex = root?.stateIndex ?: return - val senderId = root?.sender ?: return - val chunkEntity = chunk?.firstOrNull() ?: return - val isUnlinked = chunkEntity.isUnlinked() - var senderMembershipEvent: EventEntity? - var senderRoomMemberContent: String? - var senderRoomMemberPrevContent: String? - when { - stateIndex <= 0 -> { - senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root - senderRoomMemberContent = senderMembershipEvent?.prevContent - senderRoomMemberPrevContent = senderMembershipEvent?.content - } - else -> { - senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root - senderRoomMemberContent = senderMembershipEvent?.content - senderRoomMemberPrevContent = senderMembershipEvent?.prevContent - } - } - - // We fallback to untimelinedStateEvents if we can't find membership events in timeline - if (senderMembershipEvent == null) { - senderMembershipEvent = roomEntity.untimelinedStateEvents - .where() - .equalTo(EventEntityFields.STATE_KEY, senderId) - .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) - .prev(since = stateIndex) - senderRoomMemberContent = senderMembershipEvent?.content - senderRoomMemberPrevContent = senderMembershipEvent?.prevContent - } - - ContentMapper.map(senderRoomMemberContent).toModel()?.also { - this.senderAvatar = it.avatarUrl - this.senderName = it.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) - } - - // We try to fallback on prev content if we got a room member state events with null fields - if (root?.type == EventType.STATE_ROOM_MEMBER) { - ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { - if (this.senderAvatar == null && it.avatarUrl != null) { - this.senderAvatar = it.avatarUrl - } - if (this.senderName == null && it.displayName != null) { - this.senderName = it.displayName - this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) - } - } - } - this.senderMembershipEvent = senderMembershipEvent -} internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) @@ -93,10 +28,3 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { currentIdNum.toLong() + 1 } } - -private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { - return where() - .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender) - .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER) - .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked) -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt new file mode 100644 index 0000000000..983de3a50f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt @@ -0,0 +1,148 @@ +/* + * 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.helper + +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.query.next +import im.vector.matrix.android.internal.database.query.prev +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.extensions.assertIsManaged +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import io.realm.RealmList +import io.realm.RealmQuery +import javax.inject.Inject + +/** + * This is an internal cache to avoid querying all the time the room member events + */ +@SessionScope +internal class TimelineEventSenderVisitor @Inject constructor() { + + internal data class Key( + val roomId: String, + val stateIndex: Int, + val senderId: String + ) + + internal class Value( + var senderAvatar: String? = null, + var senderName: String? = null, + var isUniqueDisplayName: Boolean = false, + var senderMembershipEventId: String? = null + ) + + private val values = HashMap() + + fun clear() { + values.clear() + } + + fun clear(roomId: String, senderId: String) { + val keysToRemove = values.keys.filter { it.senderId == senderId && it.roomId == roomId } + keysToRemove.forEach { + values.remove(it) + } + } + + fun visit(timelineEventEntities: List) = timelineEventEntities.forEach { visit(it) } + + fun visit(timelineEventEntity: TimelineEventEntity) { + if (!timelineEventEntity.isValid) { + return + } + val key = Key( + roomId = timelineEventEntity.roomId, + stateIndex = timelineEventEntity.root?.stateIndex ?: 0, + senderId = timelineEventEntity.root?.sender ?: "" + ) + val result = values.getOrPut(key) { + timelineEventEntity.computeValue() + } + timelineEventEntity.apply { + this.isUniqueDisplayName = result.isUniqueDisplayName + this.senderAvatar = result.senderAvatar + this.senderName = result.senderName + this.senderMembershipEventId = result.senderMembershipEventId + } + } + + private fun RealmList.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery { + return where() + .equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender) + .equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER) + .equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked) + } + + private fun TimelineEventEntity.computeValue(): Value { + assertIsManaged() + val result = Value() + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return result + val stateIndex = root?.stateIndex ?: return result + val senderId = root?.sender ?: return result + val chunkEntity = chunk?.firstOrNull() ?: return result + val isUnlinked = chunkEntity.isUnlinked + var senderMembershipEvent: EventEntity? + var senderRoomMemberContent: String? + var senderRoomMemberPrevContent: String? + + if (stateIndex <= 0) { + senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root + senderRoomMemberContent = senderMembershipEvent?.prevContent + senderRoomMemberPrevContent = senderMembershipEvent?.content + } else { + senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root + senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + // We fallback to untimelinedStateEvents if we can't find membership events in timeline + if (senderMembershipEvent == null) { + senderMembershipEvent = roomEntity.untimelinedStateEvents + .where() + .equalTo(EventEntityFields.STATE_KEY, senderId) + .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .prev(since = stateIndex) + senderRoomMemberContent = senderMembershipEvent?.content + senderRoomMemberPrevContent = senderMembershipEvent?.prevContent + } + + ContentMapper.map(senderRoomMemberContent).toModel()?.also { + result.senderAvatar = it.avatarUrl + result.senderName = it.displayName + result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + // We try to fallback on prev content if we got a room member state events with null fields + if (root?.type == EventType.STATE_ROOM_MEMBER) { + ContentMapper.map(senderRoomMemberPrevContent).toModel()?.also { + if (result.senderAvatar == null && it.avatarUrl != null) { + result.senderAvatar = it.avatarUrl + } + if (result.senderName == null && it.displayName != null) { + result.senderName = it.displayName + result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + } + } + } + result.senderMembershipEventId = senderMembershipEvent?.eventId + return result + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt new file mode 100644 index 0000000000..a458c5e506 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt @@ -0,0 +1,36 @@ +/* + * 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.mapper + +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.internal.database.model.RoomMemberEntity + +internal object RoomMemberMapper { + + fun map(roomMemberEntity: RoomMemberEntity): RoomMember { + return RoomMember( + userId = roomMemberEntity.userId, + avatarUrl = roomMemberEntity.avatarUrl, + displayName = roomMemberEntity.displayName, + membership = roomMemberEntity.membership + ) + } +} + +internal fun RoomMemberEntity.asDomain(): RoomMember { + return RoomMemberMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index eeb340eacb..7d25a846ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor( readMarkerId = roomSummaryEntity.readMarkerId, userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), canonicalAlias = roomSummaryEntity.canonicalAlias, - aliases = roomSummaryEntity.aliases.toList() + aliases = roomSummaryEntity.aliases.toList(), + isEncrypted = roomSummaryEntity.isEncrypted ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt index 577c391b3a..94d4a9043f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ChunkEntity.kt @@ -30,7 +30,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, var backwardsDisplayIndex: Int? = null, var forwardsDisplayIndex: Int? = null, var backwardsStateIndex: Int? = null, - var forwardsStateIndex: Int? = null + var forwardsStateIndex: Int? = null, + var isUnlinked: Boolean = false ) : RealmObject() { fun identifier() = "${prevToken}_$nextToken" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt new file mode 100644 index 0000000000..c532857fe1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt @@ -0,0 +1,43 @@ +/* + * 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.model + +import im.vector.matrix.android.api.session.room.model.Membership +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.PrimaryKey + +internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "", + @Index var userId: String = "", + @Index var roomId: String = "", + var displayName: String = "", + var avatarUrl: String = "", + var reason: String? = null, + var isDirect: Boolean = false +) : RealmObject() { + + private var membershipStr: String = Membership.NONE.name + var membership: Membership + get() { + return Membership.valueOf(membershipStr) + } + set(value) { + membershipStr = value.name + } + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 406c8700b6..4c99832b39 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -42,7 +42,9 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, var canonicalAlias: String? = null, var aliases: RealmList = RealmList(), - var flatAliases: String = "" + // this is required for querying + var flatAliases: String = "", + var isEncrypted: Boolean = false ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 6059d3faf7..07ff1df005 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -49,6 +49,7 @@ import io.realm.annotations.RealmModule ReadMarkerEntity::class, UserDraftsEntity::class, DraftEntity::class, - HomeServerCapabilitiesEntity::class + HomeServerCapabilitiesEntity::class, + RoomMemberEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt index 235910b1ea..22f4b9c506 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/TimelineEventEntity.kt @@ -29,7 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0, var senderName: String? = null, var isUniqueDisplayName: Boolean = false, var senderAvatar: String? = null, - var senderMembershipEvent: EventEntity? = null, + var senderMembershipEventId: String? = null, var readReceipts: ReadReceiptsSummaryEntity? = null ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index 69402ac1de..b8c058e667 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() } -internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity { +internal fun ChunkEntity.Companion.create( + realm: Realm, + prevToken: String?, + nextToken: String?, + isUnlinked: Boolean +): ChunkEntity { return realm.createObject().apply { this.prevToken = prevToken this.nextToken = nextToken + this.isUnlinked = isUnlinked } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt new file mode 100644 index 0000000000..2ddade0048 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt @@ -0,0 +1,34 @@ +/* + * 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.query + +import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { + val query = realm + .where() + .equalTo(RoomMemberEntityFields.ROOM_ID, roomId) + + if (userId != null) { + query.equalTo(RoomMemberEntityFields.USER_ID, userId) + } + return query +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 3bd035c0b1..221e8ccb46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -54,7 +54,7 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List { return realm.where() - .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId) + .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId) .findAll() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt similarity index 53% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt index 0f5bf2a8c5..2bc05eacec 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryEnumListProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 2020 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. @@ -14,12 +14,20 @@ * limitations under the License. */ -package im.vector.riotx.features.home.room.detail.composer +package im.vector.matrix.android.internal.query -import im.vector.riotx.core.platform.VectorViewModelAction +import io.realm.RealmObject +import io.realm.RealmQuery -sealed class TextComposerAction : VectorViewModelAction { - data class QueryUsers(val query: CharSequence?) : TextComposerAction() - data class QueryRooms(val query: CharSequence?) : TextComposerAction() - data class QueryGroups(val query: CharSequence?) : TextComposerAction() +fun > RealmQuery.process(field: String, enums: List>): RealmQuery { + val lastEnumValue = enums.lastOrNull() + beginGroup() + for (enumValue in enums) { + equalTo(field, enumValue.name) + if (enumValue != lastEnumValue) { + or() + } + } + endGroup() + return this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt new file mode 100644 index 0000000000..ebe10cad9c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/query/QueryStringValueProcessor.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.query + +import im.vector.matrix.android.api.query.QueryStringValue +import io.realm.Case +import io.realm.RealmObject +import io.realm.RealmQuery +import timber.log.Timber + +fun RealmQuery.process(field: String, queryStringValue: QueryStringValue): RealmQuery { + when (queryStringValue) { + is QueryStringValue.NoCondition -> Timber.v("No condition to process") + is QueryStringValue.IsNotNull -> isNotNull(field) + is QueryStringValue.IsNull -> isNull(field) + is QueryStringValue.IsEmpty -> isEmpty(field) + is QueryStringValue.IsNotEmpty -> isNotEmpty(field) + is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase()) + } + return this +} + +private fun QueryStringValue.Case.toRealmCase(): Case { + return when (this) { + QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE + QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index d52379eb6e..b0bf70eb70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -156,7 +156,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se syncTaskSequencer.close() } - override fun syncState(): LiveData { + override fun getSyncStateLive(): LiveData { return getSyncThread().liveState() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt index 192c6fe40c..baa8f5218d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/DefaultGroupService.kt @@ -20,12 +20,16 @@ import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.util.fetchCopyMap +import io.realm.Realm +import io.realm.RealmQuery import javax.inject.Inject internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { @@ -41,10 +45,23 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon ) } - override fun liveGroupSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, + override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { groupSummariesQuery(it, groupSummaryQueryParams) }, { it.asDomain() } ) } + + override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { groupSummariesQuery(it, groupSummaryQueryParams) }, + { it.asDomain() } + ) + } + + private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery { + return GroupSummaryEntity.where(realm) + .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index 8c7e9fb263..fcce69c2fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -86,7 +86,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con .executeBy(taskExecutor) } - override fun livePushers(): LiveData> { + override fun getPushersLive(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> PusherEntity.where(realm) }, { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index b53fa3ce33..0cfc5aad3c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams @@ -32,6 +33,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.findByAlias import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask @@ -41,6 +43,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopyMap import io.realm.Realm +import io.realm.RealmQuery import javax.inject.Inject internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, @@ -86,30 +89,51 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona }) } - override fun liveRoomSummaries(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> - RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - }, + override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List { + return monarchy.fetchAllMappedSync( + { roomSummariesQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } - override fun liveBreadcrumbs(): LiveData> { + override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( - { realm -> - RoomSummaryEntity.where(realm) - .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) - .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) - .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) - }, + { roomSummariesQuery(it, queryParams) }, { roomSummaryMapper.map(it) } ) } + private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery { + val query = RoomSummaryEntity.where(realm) + query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) + query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) + query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + return query + } + + override fun getBreadcrumbs(): List { + return monarchy.fetchAllMappedSync( + { breadcrumbsQuery(it) }, + { roomSummaryMapper.map(it) } + ) + } + + override fun getBreadcrumbsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { breadcrumbsQuery(it) }, + { roomSummaryMapper.map(it) } + ) + } + + private fun breadcrumbsQuery(realm: Realm): RealmQuery { + return RoomSummaryEntity.where(realm) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) + .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) + } + override fun onRoomDisplayed(roomId: String): Cancelable { return updateBreadcrumbsTask .configureWith(UpdateBreadcrumbsTask.Params(roomId)) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index c9d5aeb6bb..0bb2dc0f27 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -20,10 +20,9 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomAvatarContent -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId @@ -47,19 +46,15 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona return@doWithRealm } val roomMembers = RoomMembers(realm, roomId) - val members = roomMembers.queryRoomMembersEvent().findAll() + val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) if (members.size == 1) { - res = members.firstOrNull()?.toRoomMember()?.avatarUrl + res = members.firstOrNull()?.avatarUrl } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst() - res = firstOtherMember?.toRoomMember()?.avatarUrl + val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst() + res = firstOtherMember?.avatarUrl } } return res } - - private fun EventEntity?.toRoomMember(): RoomMember? { - return ContentMapper.map(this?.content).toModel() - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index e9f33a547b..ea5c2e858c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -24,8 +24,8 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.* @@ -93,10 +93,11 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() + val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev() roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 - // avoid this call if we are sure there are unread events - || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) + // avoid this call if we are sure there are unread events + || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) @@ -105,18 +106,20 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() ?.canonicalAlias - val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases ?: emptyList() + val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases + ?: emptyList() roomSummaryEntity.aliases.clear() roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.isEncrypted = encryptionEvent != null if (updateMembers) { val otherRoomMembers = RoomMembers(realm, roomId) .queryRoomMembersEvent() - .notEqualTo(EventEntityFields.STATE_KEY, userId) + .notEqualTo(RoomMemberEntityFields.USER_ID, userId) .findAll() .asSequence() - .map { it.stateKey } + .map { it.userId } roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index 00c1c2c4ca..679f4a050b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -21,18 +21,23 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.members.MembershipService +import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied +import io.realm.Realm +import io.realm.RealmQuery internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String, private val monarchy: Monarchy, @@ -58,29 +63,44 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr } override fun getRoomMember(userId: String): RoomMember? { - val eventEntity = monarchy.fetchCopied { - RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst() + val roomMemberEntity = monarchy.fetchCopied { + RoomMembers(it, roomId).getLastRoomMember(userId) } - return eventEntity?.asDomain()?.content.toModel() + return roomMemberEntity?.asDomain() } - override fun getRoomMemberIdsLive(): LiveData> { - return monarchy.findAllMappedWithChanges( + override fun getRoomMembers(queryParams: RoomMemberQueryParams): List { + return monarchy.fetchAllMappedSync( { - RoomMembers(it, roomId).queryRoomMembersEvent() + roomMembersQuery(it, queryParams) }, { - it.stateKey!! + it.asDomain() } ) } + override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> { + return monarchy.findAllMappedWithChanges( + { + roomMembersQuery(it, queryParams) + }, + { + it.asDomain() + } + ) + } + + private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { + return RoomMembers(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName) + } + override fun getNumberOfJoinedMembers(): Int { - var result = 0 - monarchy.runTransactionSync { - result = RoomMembers(it, roomId).getNumberOfJoinedMembers() + return Realm.getInstance(monarchy.realmConfiguration).use { + RoomMembers(it, roomId).getNumberOfJoinedMembers() } - return result } override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index 7d9332ee84..dd91875f98 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -18,15 +18,14 @@ package im.vector.matrix.android.internal.session.room.membership import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor import im.vector.matrix.android.internal.database.helper.addStateEvent -import im.vector.matrix.android.internal.database.helper.updateSenderData import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.sync.SyncTokenStore -import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm @@ -44,7 +43,9 @@ internal interface LoadRoomMembersTask : Task internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI, private val monarchy: Monarchy, private val syncTokenStore: SyncTokenStore, - private val roomSummaryUpdater: RoomSummaryUpdater + private val roomSummaryUpdater: RoomSummaryUpdater, + private val roomMemberEventHandler: RoomMemberEventHandler, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { @@ -66,12 +67,11 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP for (roomMemberEvent in response.roomMemberEvents) { roomEntity.addStateEvent(roomMemberEvent) - UserEntityFactory.createOrNull(roomMemberEvent)?.also { - realm.insertOrUpdate(it) - } + roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } + timelineEventSenderVisitor.clear() roomEntity.chunks.flatMap { it.timelineEvents }.forEach { - it.updateSenderData() + timelineEventSenderVisitor.visit(it) } roomEntity.areAllMembersLoaded = true roomSummaryUpdater.update(realm, roomId, updateMembers = true) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 21270308ed..9382fbc54a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -23,9 +23,10 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where @@ -75,43 +76,46 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } val roomMembers = RoomMembers(realm, roomId) - val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() + val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() if (roomEntity?.membership == Membership.INVITE) { - val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst() + val inviteMeEvent = roomMembers.getLastStateEvent(userId) val inviterId = inviteMeEvent?.sender name = if (inviterId != null) { - val inviterMemberEvent = loadedMembers.where() - .equalTo(EventEntityFields.STATE_KEY, inviterId) + activeMembers.where() + .equalTo(RoomMemberEntityFields.USER_ID, inviterId) .findFirst() - inviterMemberEvent?.toRoomMember()?.displayName + ?.displayName } else { context.getString(R.string.room_displayname_room_invite) } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { - roomSummary.heroes.mapNotNull { - roomMembers.getStateEvent(it) + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + roomSummary.heroes.mapNotNull { userId -> + roomMembers.getLastRoomMember(userId)?.takeIf { + it.membership == Membership.INVITE || it.membership == Membership.JOIN + } } } else { - loadedMembers.where() - .notEqualTo(EventEntityFields.STATE_KEY, userId) + activeMembers.where() + .notEqualTo(RoomMemberEntityFields.USER_ID, userId) .limit(3) .findAll() + .createSnapshot() } - val otherMembersCount = roomMembers.getNumberOfMembers() - 1 + val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { 0 -> context.getString(R.string.room_displayname_empty_room) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 2 -> context.getString(R.string.room_displayname_two_members, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - resolveRoomMemberName(otherMembersSubset[1], roomMembers) + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, - roomMembers.getNumberOfJoinedMembers() - 1, - resolveRoomMemberName(otherMembersSubset[0], roomMembers), - roomMembers.getNumberOfJoinedMembers() - 1) + roomMembers.getNumberOfJoinedMembers() - 1, + resolveRoomMemberName(otherMembersSubset[0], roomMembers), + roomMembers.getNumberOfJoinedMembers() - 1) } } return@doWithRealm @@ -119,19 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - private fun resolveRoomMemberName(eventEntity: EventEntity?, + private fun resolveRoomMemberName(roomMember: RoomMemberEntity?, roomMembers: RoomMembers): String? { - if (eventEntity == null) return null - val roomMember = eventEntity.toRoomMember() ?: return null + if (roomMember == null) return null val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) return if (isUnique) { roomMember.displayName } else { - "${roomMember.displayName} (${eventEntity.stateKey})" + "${roomMember.displayName} (${roomMember.userId})" } } - - private fun EventEntity?.toRoomMember(): RoomMember? { - return ContentMapper.map(this?.content).toModel() - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt new file mode 100644 index 0000000000..51df244401 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.session.room.membership + +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.database.model.RoomMemberEntity + +internal object RoomMemberEntityFactory { + + fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity { + val primaryKey = "${roomId}_$userId" + return RoomMemberEntity( + primaryKey = primaryKey, + userId = userId, + roomId = roomId, + displayName = roomMember.displayName ?: "", + avatarUrl = roomMember.avatarUrl ?: "" + ).apply { + membership = roomMember.membership + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt new file mode 100644 index 0000000000..9bd97cec10 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEventHandler.kt @@ -0,0 +1,44 @@ +/* + * 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.session.room.membership + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMemberContent +import im.vector.matrix.android.internal.session.user.UserEntityFactory +import io.realm.Realm +import javax.inject.Inject + +internal class RoomMemberEventHandler @Inject constructor() { + + fun handle(realm: Realm, roomId: String, event: Event): Boolean { + if (event.type != EventType.STATE_ROOM_MEMBER) { + return false + } + val roomMember = event.content.toModel() ?: return false + val userId = event.stateKey ?: return false + val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) + realm.insertOrUpdate(roomMemberEntity) + if (roomMember.membership in Membership.activeMemberships()) { + val userEntity = UserEntityFactory.create(userId, roomMember) + realm.insertOrUpdate(userEntity) + } + return true + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index 9fba1d8f02..e3775f5ade 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -17,12 +17,10 @@ package im.vector.matrix.android.internal.session.room.membership import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import io.realm.Realm @@ -42,19 +40,18 @@ internal class RoomMembers(private val realm: Realm, RoomSummaryEntity.where(realm, roomId).findFirst() } - fun getStateEvent(userId: String): EventEntity? { + fun getLastStateEvent(userId: String): EventEntity? { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .equalTo(EventEntityFields.STATE_KEY, userId) + .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) .findFirst() } - fun get(userId: String): RoomMember? { - return getStateEvent(userId) - ?.let { - it.asDomain().content?.toModel() - } + fun getLastRoomMember(userId: String): RoomMemberEntity? { + return RoomMemberEntity + .where(realm, roomId, userId) + .findFirst() } fun isUniqueDisplayName(displayName: String?): Boolean { @@ -69,36 +66,37 @@ internal class RoomMembers(private val realm: Realm, .size == 1 } - fun queryRoomMembersEvent(): RealmQuery { - return EventEntity - .where(realm, roomId, EventType.STATE_ROOM_MEMBER) - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) - .isNotNull(EventEntityFields.STATE_KEY) - .distinct(EventEntityFields.STATE_KEY) - .isNotNull(EventEntityFields.CONTENT) + fun queryRoomMembersEvent(): RealmQuery { + return RoomMemberEntity.where(realm, roomId) } - fun queryJoinedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"") - } - - fun queryInvitedRoomMembersEvent(): RealmQuery { - return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"") - } - - fun queryRoomMemberEvent(userId: String): RealmQuery { + fun queryJoinedRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() - .equalTo(EventEntityFields.STATE_KEY, userId) + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + } + + fun queryInvitedRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + } + + fun queryActiveRoomMembersEvent(): RealmQuery { + return queryRoomMembersEvent() + .beginGroup() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .or() + .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .endGroup() } fun getNumberOfJoinedMembers(): Int { return roomSummary?.joinedMembersCount - ?: queryJoinedRoomMembersEvent().findAll().size + ?: queryJoinedRoomMembersEvent().findAll().size } fun getNumberOfInvitedMembers(): Int { return roomSummary?.invitedMembersCount - ?: queryInvitedRoomMembersEvent().findAll().size + ?: queryInvitedRoomMembersEvent().findAll().size } fun getNumberOfMembers(): Int { @@ -111,7 +109,7 @@ internal class RoomMembers(private val realm: Realm, * @return a roomMember id list of joined or invited members. */ fun getActiveRoomMemberIds(): List { - return getRoomMemberIdsFiltered { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + return queryActiveRoomMembersEvent().findAll().map { it.userId } } /** @@ -120,21 +118,6 @@ internal class RoomMembers(private val realm: Realm, * @return a roomMember id list of joined members. */ fun getJoinedRoomMemberIds(): List { - return getRoomMemberIdsFiltered { it.membership == Membership.JOIN } - } - - /* ========================================================================================== - * Private - * ========================================================================================== */ - - private fun getRoomMemberIdsFiltered(predicate: (RoomMember) -> Boolean): List { - return RoomMembers(realm, roomId) - .queryRoomMembersEvent() - .findAll() - .map { it.asDomain() } - .associateBy { it.stateKey!! } - .filterValues { predicate(it.content.toModel()!!) } - .keys - .toList() + return queryJoinedRoomMembersEvent().findAll().map { it.userId } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt index de3eb1eab2..8228136f10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.UnsignedData -import im.vector.matrix.android.internal.database.helper.updateSenderData +import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EventEntity @@ -41,7 +41,8 @@ internal interface PruneEventTask : Task { ) } -internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask { +internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor) : PruneEventTask { override suspend fun execute(params: PruneEventTask.Params) { monarchy.awaitTransaction { realm -> @@ -65,12 +66,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return - val allowedKeys = computeAllowedKeys(eventToPrune.type) + val typeToPrune = eventToPrune.type + val stateKey = eventToPrune.stateKey + val allowedKeys = computeAllowedKeys(typeToPrune) if (allowedKeys.isNotEmpty()) { val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } eventToPrune.content = ContentMapper.map(prunedContent) } else { - when (eventToPrune.type) { + when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") @@ -94,11 +97,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M // } } } - if (eventToPrune.type == EventType.STATE_ROOM_MEMBER) { + if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { + timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey) val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) - for (timelineEvent in timelineEventsToUpdate) { - timelineEvent.updateSenderData() - } + timelineEventSenderVisitor.visit(timelineEventsToUpdate) } } 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 8731045e14..180776ba8d 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 @@ -215,7 +215,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } - override fun getEventSummaryLive(eventId: String): LiveData> { + override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { + return monarchy.fetchCopyMap( + { EventAnnotationsSummaryEntity.where(it, eventId).findFirst() }, + { entity, _ -> + entity.asDomain() + } + ) + } + + override fun getEventAnnotationsSummaryLive(eventId: String): LiveData> { val liveData = monarchy.findAllMappedWithChanges( { EventAnnotationsSummaryEntity.where(it, eventId) }, { it.asDomain() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt index 04cf810fe4..b532d61914 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/ClearUnlinkedEventsTask.kt @@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction @@ -38,7 +37,7 @@ internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val mo monarchy.awaitTransaction { localRealm -> val unlinkedChunks = ChunkEntity .where(localRealm, roomId = params.roomId) - .equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) + .equalTo(ChunkEntityFields.IS_UNLINKED, true) .findAll() unlinkedChunks.forEach { it.deleteOnCascade() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 85bab5d706..057295ec44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,13 +27,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where @@ -44,16 +38,10 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber -import java.util.Collections -import java.util.UUID +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList @@ -77,11 +65,11 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts ) : Timeline, TimelineHiddenReadReceipts.Delegate { - private companion object { + companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") } - private val listeners = ArrayList() + private val listeners = CopyOnWriteArrayList() private val isStarted = AtomicBoolean(false) private val isReady = AtomicBoolean(false) private val mainHandler = createUIHandler() @@ -113,11 +101,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { - handleInitialLoad() - } else { - handleUpdates(changeSet) - } + handleUpdates(changeSet) } private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> @@ -179,8 +163,9 @@ internal class DefaultTimeline( nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() - .findAllAsync() - .also { it.addChangeListener(eventsChangeListener) } + .findAll() + handleInitialLoad() + filteredEvents.addChangeListener(eventsChangeListener) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) .findAllAsync() @@ -288,20 +273,20 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } - override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { + override fun addListener(listener: Timeline.Listener): Boolean { if (listeners.contains(listener)) { return false } - listeners.add(listener).also { + return listeners.add(listener).also { postSnapshot() } } - override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { - listeners.remove(listener) + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) } - override fun removeAllListeners() = synchronized(listeners) { + override fun removeAllListeners() { listeners.clear() } @@ -402,14 +387,14 @@ internal class DefaultTimeline( private fun getState(direction: Timeline.Direction): State { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } private fun updateState(direction: Timeline.Direction, update: (State) -> State) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() @@ -508,10 +493,10 @@ internal class DefaultTimeline( this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> @@ -656,10 +641,8 @@ internal class DefaultTimeline( updateLoadingStates(filteredEvents) val snapshot = createSnapshot() val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onTimelineUpdated(snapshot) - } + listeners.forEach { + it.onTimelineUpdated(snapshot) } } debouncer.debounce("post_snapshot", runnable, 50) @@ -671,10 +654,8 @@ internal class DefaultTimeline( return } val runnable = Runnable { - synchronized(listeners) { - listeners.forEach { - it.onTimelineFailure(throwable) - } + listeners.forEach { + it.onTimelineFailure(throwable) } } mainHandler.post(runnable) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 7030509bfc..87c59e832b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -18,17 +18,12 @@ package im.vector.matrix.android.internal.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.internal.database.helper.* -import im.vector.matrix.android.internal.database.helper.add -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvent -import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.user.UserEntityFactory import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.kotlin.createObject import timber.log.Timber @@ -37,7 +32,8 @@ import javax.inject.Inject /** * Insert Chunk in DB, and eventually merge with existing chunk event */ -internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) { +internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy, + private val timelineEventSenderVisitor: TimelineEventSenderVisitor) { /** *
@@ -136,27 +132,22 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
 
                     // The current chunk is the one we will keep all along the merge processChanges.
                     // We try to look for a chunk next to the token,
-                    // otherwise we create a whole new one
+                    // otherwise we create a whole new one which is unlinked (not live)
 
                     var currentChunk = if (direction == PaginationDirection.FORWARDS) {
                         prevChunk?.apply { this.nextToken = nextToken }
                     } else {
                         nextChunk?.apply { this.prevToken = prevToken }
                     }
-                            ?: ChunkEntity.create(realm, prevToken, nextToken)
+                            ?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
 
                     if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
                         Timber.v("Reach end of $roomId")
                         currentChunk.isLastBackward = true
                     } else if (!shouldSkip) {
                         Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
-                        val eventIds = ArrayList(receivedChunk.events.size)
-                        for (event in receivedChunk.events) {
-                            event.eventId?.also { eventIds.add(it) }
-                            currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
-                            UserEntityFactory.createOrNull(event)?.also {
-                                realm.insertOrUpdate(it)
-                            }
+                        val timelineEvents = receivedChunk.events.mapNotNull {
+                            currentChunk.add(roomId, it, direction)
                         }
                         // Then we merge chunks if needed
                         if (currentChunk != prevChunk && prevChunk != null) {
@@ -174,12 +165,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
                         }
                         roomEntity.addOrUpdate(currentChunk)
                         for (stateEvent in receivedChunk.stateEvents) {
-                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
-                            UserEntityFactory.createOrNull(stateEvent)?.also {
-                                realm.insertOrUpdate(it)
-                            }
+                            roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
                         }
-                        currentChunk.updateSenderDataFor(eventIds)
+                        timelineEventSenderVisitor.visit(timelineEvents)
                     }
                 }
         return if (receivedChunk.events.isEmpty()) {
@@ -200,11 +188,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
         // We always merge the bottom chunk into top chunk, so we are always merging backwards
         Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
         return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
-            currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
+            val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
+            timelineEventSenderVisitor.visit(events)
             roomEntity.deleteOnCascade(otherChunk)
             currentChunk
         } else {
-            otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
+            val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
+            timelineEventSenderVisitor.visit(events)
             roomEntity.deleteOnCascade(currentChunk)
             otherChunk
         }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
index a080a5158e..488b9ce83d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt
@@ -27,16 +27,17 @@ import im.vector.matrix.android.internal.database.helper.*
 import im.vector.matrix.android.internal.database.model.ChunkEntity
 import im.vector.matrix.android.internal.database.model.EventEntityFields
 import im.vector.matrix.android.internal.database.model.RoomEntity
+import im.vector.matrix.android.internal.database.model.TimelineEventEntity
 import im.vector.matrix.android.internal.database.query.find
 import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
 import im.vector.matrix.android.internal.session.mapWithProgress
 import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
+import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
 import im.vector.matrix.android.internal.session.room.read.FullyReadContent
 import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
 import im.vector.matrix.android.internal.session.sync.model.*
-import im.vector.matrix.android.internal.session.user.UserEntityFactory
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import timber.log.Timber
@@ -46,7 +47,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val roomSummaryUpdater: RoomSummaryUpdater,
                                                    private val roomTagHandler: RoomTagHandler,
                                                    private val roomFullyReadHandler: RoomFullyReadHandler,
-                                                   private val cryptoService: DefaultCryptoService) {
+                                                   private val cryptoService: DefaultCryptoService,
+                                                   private val roomMemberEventHandler: RoomMemberEventHandler,
+                                                   private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map) : HandlingStrategy()
@@ -119,9 +122,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
                 // Give info to crypto module
                 cryptoService.onStateEvent(roomId, event)
-                UserEntityFactory.createOrNull(event)?.also {
-                    realm.insertOrUpdate(it)
-                }
+                roomMemberEventHandler.handle(realm, roomId, event)
             }
         }
         if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
@@ -189,11 +190,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         }
         lastChunk?.isLastForward = false
         chunkEntity.isLastForward = true
+        chunkEntity.isUnlinked = false
 
-        val eventIds = ArrayList(eventList.size)
+        val timelineEvents = ArrayList(eventList.size)
         for (event in eventList) {
-            event.eventId?.also { eventIds.add(it) }
-            chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
+            chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also {
+                timelineEvents.add(it)
+            }
             // Give info to crypto module
             cryptoService.onLiveEvent(roomEntity.roomId, event)
             // Try to remove local echo
@@ -206,11 +209,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     Timber.v("Can't find corresponding local echo for tx:$it")
                 }
             }
-            UserEntityFactory.createOrNull(event)?.also {
-                realm.insertOrUpdate(it)
-            }
+            roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
         }
-        chunkEntity.updateSenderDataFor(eventIds)
+        timelineEventSenderVisitor.visit(timelineEvents)
         return chunkEntity
     }
 
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt
index 35988e6c6f..9bc8c86be5 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt
@@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.api.pushrules.RuleScope
 import im.vector.matrix.android.api.pushrules.RuleSetKey
 import im.vector.matrix.android.api.session.events.model.toModel
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
 import im.vector.matrix.android.internal.database.mapper.asDomain
 import im.vector.matrix.android.internal.database.model.*
@@ -69,9 +69,9 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
         var hasUpdate = false
         monarchy.doWithRealm { realm ->
             invites.forEach { (roomId, _) ->
-                val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId)
+                val myUserStateEvent = RoomMembers(realm, roomId).getLastStateEvent(userId)
                 val inviterId = myUserStateEvent?.sender
-                val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
+                val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
                 val isDirect = myUserRoomMember?.isDirect
                 if (inviterId != null && inviterId != userId && isDirect == true) {
                     directChats
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
index 37bcc225c1..9fe3e38d36 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
@@ -112,7 +112,7 @@ abstract class SyncService : Service() {
         try {
             syncTask.execute(params)
             // Start sync if we were doing an initial sync and the syncThread is not launched yet
-            if (isInitialSync && session.syncState().value == SyncState.Idle) {
+            if (isInitialSync && session.getSyncStateLive().value == SyncState.Idle) {
                 val isForeground = !backgroundDetectionObserver.isInBackground
                 session.startSync(isForeground)
             }
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 d314c8d108..761c810b41 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
@@ -70,7 +70,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
         return userEntity.asDomain()
     }
 
-    override fun liveUser(userId: String): LiveData> {
+    override fun getUserLive(userId: String): LiveData> {
         val liveData = monarchy.findAllMappedWithChanges(
                 { UserEntity.where(it, userId) },
                 { it.asDomain() }
@@ -80,7 +80,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
         }
     }
 
-    override fun liveUsers(): LiveData> {
+    override fun getUsersLive(): LiveData> {
         return monarchy.findAllMappedWithChanges(
                 { realm ->
                     realm.where(UserEntity::class.java)
@@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
         )
     }
 
-    override fun livePagedUsers(filter: String?): LiveData> {
+    override fun getPagedUsersLive(filter: String?): LiveData> {
         realmDataSourceFactory.updateQuery { realm ->
             val query = realm.where(UserEntity::class.java)
             if (filter.isNullOrEmpty()) {
@@ -121,7 +121,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
                 .executeBy(taskExecutor)
     }
 
-    override fun liveIgnoredUsers(): LiveData> {
+    override fun getIgnoredUsersLive(): LiveData> {
         return monarchy.findAllMappedWithChanges(
                 { realm ->
                     realm.where(IgnoredUserEntity::class.java)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt
index 2ded32b7db..f931db1cff 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserEntityFactory.kt
@@ -16,27 +16,16 @@
 
 package im.vector.matrix.android.internal.session.user
 
-import im.vector.matrix.android.api.session.events.model.Event
-import im.vector.matrix.android.api.session.events.model.EventType
-import im.vector.matrix.android.api.session.events.model.toModel
-import im.vector.matrix.android.api.session.room.model.Membership
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 import im.vector.matrix.android.internal.database.model.UserEntity
 
 internal object UserEntityFactory {
 
-    fun createOrNull(event: Event): UserEntity? {
-        if (event.type != EventType.STATE_ROOM_MEMBER) {
-            return null
-        }
-        val roomMember = event.content.toModel() ?: return null
-        // We only use JOIN and INVITED memberships to create User data
-        if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) {
-            return null
-        }
-        return UserEntity(event.stateKey ?: "",
-                roomMember.displayName ?: "",
-                roomMember.avatarUrl ?: ""
+    fun create(userId: String, roomMember: RoomMemberContent): UserEntity {
+        return UserEntity(
+                userId = userId,
+                displayName = roomMember.displayName ?: "",
+                avatarUrl = roomMember.avatarUrl ?: ""
         )
     }
 }
diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
index a29f5d5542..1d1bbe1406 100644
--- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
+++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/api/pushrules/PushrulesConditionTest.kt
@@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
 import im.vector.matrix.android.api.session.room.Room
 import im.vector.matrix.android.api.session.room.RoomService
 import im.vector.matrix.android.api.session.room.model.Membership
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
 import io.mockk.every
 import io.mockk.mockk
@@ -40,7 +40,7 @@ class PushrulesConditionTest {
                 content = MessageTextContent("m.text", "Yo wtf?").toContent(),
                 originServerTs = 0)
 
-        val rm = RoomMember(
+        val rm = RoomMemberContent(
                 Membership.INVITE,
                 displayName = "Foo",
                 avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
@@ -72,7 +72,7 @@ class PushrulesConditionTest {
                 type = "m.room.member",
                 eventId = "mx0",
                 stateKey = "@foo:matrix.org",
-                content = RoomMember(
+                content = RoomMemberContent(
                         Membership.INVITE,
                         displayName = "Foo",
                         avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
diff --git a/vector/build.gradle b/vector/build.gradle
index 513449db51..9bab8596b1 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -218,8 +218,8 @@ android {
 
 dependencies {
 
-    def epoxy_version = '3.8.0'
-    def fragment_version = '1.2.0-rc01'
+    def epoxy_version = '3.9.0'
+    def fragment_version = '1.2.0-rc04'
     def arrow_version = "0.8.2"
     def coroutines_version = "1.3.2"
     def markwon_version = '4.1.2'
@@ -227,7 +227,7 @@ dependencies {
     def glide_version = '4.10.0'
     def moshi_version = '1.8.0'
     def daggerVersion = '2.24'
-    def autofill_version = "1.0.0-rc01"
+    def autofill_version = "1.0.0"
 
     implementation project(":matrix-sdk-android")
     implementation project(":matrix-sdk-android-rx")
@@ -238,11 +238,11 @@ dependencies {
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
 
+    implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01"
     implementation 'androidx.appcompat:appcompat:1.1.0'
     implementation "androidx.fragment:fragment:$fragment_version"
     implementation "androidx.fragment:fragment-ktx:$fragment_version"
-    //Do not use beta2 at the moment, as it breaks things
-    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
     implementation 'androidx.core:core-ktx:1.1.0'
 
     implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
@@ -275,10 +275,10 @@ dependencies {
     implementation 'com.airbnb.android:mvrx:1.3.0'
 
     // Work
-    implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01"
+    implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
 
     // Paging
-    implementation "androidx.paging:paging-runtime-ktx:2.1.0"
+    implementation "androidx.paging:paging-runtime-ktx:2.1.1"
 
     // Functional Programming
     implementation "io.arrow-kt:arrow-core:$arrow_version"
@@ -288,7 +288,7 @@ dependencies {
 
     // UI
     implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
-    implementation 'com.google.android.material:material:1.1.0-beta01'
+    implementation 'com.google.android.material:material:1.2.0-alpha03'
     implementation 'me.gujun.android:span:1.7'
     implementation "io.noties.markwon:core:$markwon_version"
     implementation "io.noties.markwon:html:$markwon_version"
diff --git a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
index cfbed0ee13..936253de28 100644
--- a/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
+++ b/vector/src/main/java/im/vector/riotx/AppStateHandler.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.OnLifecycleEvent
 import arrow.core.Option
 import im.vector.matrix.android.api.session.group.model.GroupSummary
 import im.vector.matrix.android.api.session.room.model.RoomSummary
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.matrix.rx.rx
 import im.vector.riotx.features.home.HomeRoomListDataSource
 import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
@@ -65,7 +66,8 @@ class AppStateHandler @Inject constructor(
                         sessionDataSource.observe()
                                 .observeOn(AndroidSchedulers.mainThread())
                                 .switchMap {
-                                    it.orNull()?.rx()?.liveRoomSummaries()
+                                    val query = roomSummaryQueryParams {}
+                                    it.orNull()?.rx()?.liveRoomSummaries(query)
                                             ?: Observable.just(emptyList())
                                 }
                                 .throttleLast(300, TimeUnit.MILLISECONDS),
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
index e0b14af9d0..aec3372098 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
@@ -63,7 +63,8 @@ import im.vector.riotx.features.ui.UiStateRepository
             ViewModelModule::class,
             FragmentModule::class,
             HomeModule::class,
-            RoomListModule::class
+            RoomListModule::class,
+            ScreenModule::class
         ]
 )
 @ScreenScope
diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt
index 1073a59f7c..56fac34f1e 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenModule.kt
@@ -17,6 +17,7 @@
 package im.vector.riotx.core.di
 
 import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.RecyclerView
 import dagger.Module
 import dagger.Provides
 import im.vector.riotx.core.glide.GlideApp
@@ -27,4 +28,9 @@ object ScreenModule {
     @Provides
     @JvmStatic
     fun providesGlideRequests(context: AppCompatActivity) = GlideApp.with(context)
+
+    @Provides
+    @JvmStatic
+    @ScreenScope
+    fun providesSharedViewPool() = RecyclerView.RecycledViewPool()
 }
diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
index b78e291506..283b43a004 100644
--- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt
@@ -44,6 +44,7 @@ import im.vector.riotx.features.notifications.*
 import im.vector.riotx.features.rageshake.BugReporter
 import im.vector.riotx.features.rageshake.VectorFileLogger
 import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
+import im.vector.riotx.features.reactions.data.EmojiDataSource
 import im.vector.riotx.features.session.SessionListener
 import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.share.ShareRoomListDataSource
@@ -124,6 +125,8 @@ interface VectorComponent {
 
     fun uiStateRepository(): UiStateRepository
 
+    fun emojiDataSource(): EmojiDataSource
+
     @Component.Factory
     interface Factory {
         fun create(@BindsInstance context: Context): VectorComponent
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt
index 667ccb1bd0..654e4c605a 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/VectorEpoxyModel.kt
@@ -18,14 +18,25 @@ package im.vector.riotx.core.epoxy
 
 import com.airbnb.epoxy.EpoxyModelWithHolder
 import com.airbnb.epoxy.VisibilityState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelChildren
 
 /**
  * EpoxyModelWithHolder which can listen to visibility state change
  */
 abstract class VectorEpoxyModel : EpoxyModelWithHolder() {
 
+    protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
+
     private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
 
+    override fun unbind(holder: H) {
+        coroutineScope.coroutineContext.cancelChildren()
+        super.unbind(holder)
+    }
+
     override fun onVisibilityStateChanged(visibilityState: Int, view: H) {
         onModelVisibilityStateChangedListener?.onVisibilityStateChanged(visibilityState)
         super.onVisibilityStateChanged(visibilityState, view)
diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
index 7b79ce8549..e5ffd5f350 100644
--- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
+++ b/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt
@@ -51,7 +51,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel= Build.VERSION_CODES.O) {
-            val notificationSubtitleRes = if (isInitialSync) {
-                R.string.notification_initial_sync
-            } else {
-                R.string.notification_listening_for_events
-            }
-            val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
-            startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
+        val notificationSubtitleRes = if (isInitialSync) {
+            R.string.notification_initial_sync
+        } else {
+            R.string.notification_listening_for_events
         }
+        val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
+        startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
     }
 
     override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) {
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt
index 010b362b68..6d498de2d2 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt
@@ -37,10 +37,6 @@ class AutocompleteEmojiController @Inject constructor(
         }
     }
 
-    init {
-        fontProvider.addListener(fontProviderListener)
-    }
-
     var listener: AutocompleteClickListener? = null
 
     override fun buildModels(data: List?) {
@@ -71,6 +67,10 @@ class AutocompleteEmojiController @Inject constructor(
         }
     }
 
+    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+        fontProvider.addListener(fontProviderListener)
+    }
+
     override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
         super.onDetachedFromRecyclerView(recyclerView)
         fontProvider.removeListener(fontProviderListener)
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
index 822ce451e7..b6f45b477c 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/group/AutocompleteGroupPresenter.kt
@@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.group
 
 import android.content.Context
 import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
 import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.group.groupSummaryQueryParams
 import im.vector.matrix.android.api.session.group.model.GroupSummary
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import javax.inject.Inject
 
 class AutocompleteGroupPresenter @Inject constructor(context: Context,
-                                                     private val controller: AutocompleteGroupController
+                                                     private val controller: AutocompleteGroupController,
+                                                     private val session: Session
 ) : RecyclerViewPresenter(context), AutocompleteClickListener {
 
-    var callback: Callback? = null
-
     init {
         controller.listener = this
     }
@@ -46,16 +46,16 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context,
     }
 
     override fun onQuery(query: CharSequence?) {
-        callback?.onQueryGroups(query)
-    }
-
-    fun render(groups: Async>) {
-        if (groups is Success) {
-            controller.setData(groups())
+        val queryParams = groupSummaryQueryParams {
+            displayName = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotEmpty
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
         }
-    }
-
-    interface Callback {
-        fun onQueryGroups(query: CharSequence?)
+        val groups = session.getGroupSummaries(queryParams)
+                .asSequence()
+                .sortedBy { it.displayName }
+        controller.setData(groups.toList())
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt
similarity index 80%
rename from vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
rename to vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt
index 53a87fe27a..1c8dc99196 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt
@@ -14,23 +14,23 @@
  * limitations under the License.
  */
 
-package im.vector.riotx.features.autocomplete.user
+package im.vector.riotx.features.autocomplete.member
 
 import com.airbnb.epoxy.TypedEpoxyController
-import im.vector.matrix.android.api.session.user.model.User
+import im.vector.matrix.android.api.session.room.model.RoomMember
 import im.vector.matrix.android.api.util.toMatrixItem
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
 import im.vector.riotx.features.home.AvatarRenderer
 import javax.inject.Inject
 
-class AutocompleteUserController @Inject constructor() : TypedEpoxyController>() {
+class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() {
 
-    var listener: AutocompleteClickListener? = null
+    var listener: AutocompleteClickListener? = null
 
     @Inject lateinit var avatarRenderer: AvatarRenderer
 
-    override fun buildModels(data: List?) {
+    override fun buildModels(data: List?) {
         if (data.isNullOrEmpty()) {
             return
         }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt
new file mode 100644
index 0000000000..84a33173b8
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.riotx.features.autocomplete.member
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
+import im.vector.matrix.android.api.session.room.model.Membership
+import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.riotx.features.autocomplete.AutocompleteClickListener
+
+class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
+                                                              @Assisted val roomId: String,
+                                                              private val session: Session,
+                                                              private val controller: AutocompleteMemberController
+) : RecyclerViewPresenter(context), AutocompleteClickListener {
+
+    private val room = session.getRoom(roomId)!!
+
+    init {
+        controller.listener = this
+    }
+
+    @AssistedInject.Factory
+    interface Factory {
+        fun create(roomId: String): AutocompleteMemberPresenter
+    }
+
+    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
+        // Also remove animation
+        recyclerView?.itemAnimator = null
+        return controller.adapter
+    }
+
+    override fun onItemClick(t: RoomMember) {
+        dispatchClick(t)
+    }
+
+    override fun onQuery(query: CharSequence?) {
+        val queryParams = roomMemberQueryParams {
+            displayName = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotEmpty
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
+            memberships = listOf(Membership.JOIN)
+        }
+        val members = room.getRoomMembers(queryParams)
+                .asSequence()
+                .sortedBy { it.displayName }
+        controller.setData(members.toList())
+    }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
index 51285b02b7..aae95502d9 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomController.kt
@@ -24,12 +24,10 @@ import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
 import im.vector.riotx.features.home.AvatarRenderer
 import javax.inject.Inject
 
-class AutocompleteRoomController @Inject constructor() : TypedEpoxyController>() {
+class AutocompleteRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : TypedEpoxyController>() {
 
     var listener: AutocompleteClickListener? = null
 
-    @Inject lateinit var avatarRenderer: AvatarRenderer
-
     override fun buildModels(data: List?) {
         if (data.isNullOrEmpty()) {
             return
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
index 53fed7f859..17787a22ef 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/room/AutocompleteRoomPresenter.kt
@@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.room
 
 import android.content.Context
 import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
 import com.otaliastudios.autocomplete.RecyclerViewPresenter
+import im.vector.matrix.android.api.query.QueryStringValue
+import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.room.model.RoomSummary
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.riotx.features.autocomplete.AutocompleteClickListener
 import javax.inject.Inject
 
 class AutocompleteRoomPresenter @Inject constructor(context: Context,
-                                                    private val controller: AutocompleteRoomController
+                                                    private val controller: AutocompleteRoomController,
+                                                    private val session: Session
 ) : RecyclerViewPresenter(context), AutocompleteClickListener {
 
-    var callback: Callback? = null
-
     init {
         controller.listener = this
     }
@@ -46,16 +46,16 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context,
     }
 
     override fun onQuery(query: CharSequence?) {
-        callback?.onQueryRooms(query)
-    }
-
-    fun render(rooms: Async>) {
-        if (rooms is Success) {
-            controller.setData(rooms())
+        val queryParams = roomSummaryQueryParams {
+            canonicalAlias = if (query.isNullOrBlank()) {
+                QueryStringValue.IsNotNull
+            } else {
+                QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+            }
         }
-    }
-
-    interface Callback {
-        fun onQueryRooms(query: CharSequence?)
+        val rooms = session.getRoomSummaries(queryParams)
+                .asSequence()
+                .sortedBy { it.displayName }
+        controller.setData(rooms.toList())
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt
deleted file mode 100644
index 01dceb5399..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserPresenter.kt
+++ /dev/null
@@ -1,61 +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.riotx.features.autocomplete.user
-
-import android.content.Context
-import androidx.recyclerview.widget.RecyclerView
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.Success
-import com.otaliastudios.autocomplete.RecyclerViewPresenter
-import im.vector.matrix.android.api.session.user.model.User
-import im.vector.riotx.features.autocomplete.AutocompleteClickListener
-import javax.inject.Inject
-
-class AutocompleteUserPresenter @Inject constructor(context: Context,
-                                                    private val controller: AutocompleteUserController
-) : RecyclerViewPresenter(context), AutocompleteClickListener {
-
-    var callback: Callback? = null
-
-    init {
-        controller.listener = this
-    }
-
-    override fun instantiateAdapter(): RecyclerView.Adapter<*> {
-        // Also remove animation
-        recyclerView?.itemAnimator = null
-        return controller.adapter
-    }
-
-    override fun onItemClick(t: User) {
-        dispatchClick(t)
-    }
-
-    override fun onQuery(query: CharSequence?) {
-        callback?.onQueryUsers(query)
-    }
-
-    fun render(users: Async>) {
-        if (users is Success) {
-            controller.setData(users())
-        }
-    }
-
-    interface Callback {
-        fun onQueryUsers(query: CharSequence?)
-    }
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
index b7c3e61ee4..c3d16f3299 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
@@ -71,6 +71,14 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
                 .into(target)
     }
 
+    @AnyThread
+    fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
+        return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
+                .onlyRetrieveFromCache(true)
+                .submit()
+                .get()
+    }
+
     @AnyThread
     fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
         val avatarColor = when (matrixItem) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
index b9d3e3c95e..85f14e99a8 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
@@ -66,6 +66,11 @@ class HomeDetailFragment @Inject constructor(
         setupToolbar()
         setupKeysBackupBanner()
 
+        withState(viewModel) {
+            // Update the navigation view if needed (for when we restore the tabs)
+            bottomNavigationView.selectedItemId = it.displayMode.toMenuId()
+        }
+
         viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary ->
             onGroupChange(groupSummary.orNull())
         }
@@ -127,7 +132,6 @@ class HomeDetailFragment @Inject constructor(
     private fun setupBottomNavigationView() {
         bottomNavigationView.setOnNavigationItemSelectedListener {
             val displayMode = when (it.itemId) {
-                R.id.bottom_action_home   -> RoomListDisplayMode.HOME
                 R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE
                 R.id.bottom_action_rooms  -> RoomListDisplayMode.ROOMS
                 else                      -> RoomListDisplayMode.HOME
@@ -149,12 +153,6 @@ class HomeDetailFragment @Inject constructor(
     private fun switchDisplayMode(displayMode: RoomListDisplayMode) {
         groupToolbarTitleView.setText(displayMode.titleRes)
         updateSelectedFragment(displayMode)
-        // Update the navigation view (for when we restore the tabs)
-        bottomNavigationView.selectedItemId = when (displayMode) {
-            RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
-            RoomListDisplayMode.ROOMS  -> R.id.bottom_action_rooms
-            else                       -> R.id.bottom_action_home
-        }
     }
 
     private fun updateSelectedFragment(displayMode: RoomListDisplayMode) {
@@ -194,4 +192,10 @@ class HomeDetailFragment @Inject constructor(
         unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
         syncStateView.render(it.syncState)
     }
+
+    private fun RoomListDisplayMode.toMenuId() = when (this) {
+        RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
+        RoomListDisplayMode.ROOMS  -> R.id.bottom_action_rooms
+        else                       -> R.id.bottom_action_home
+    }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
index 6ff836e8c8..bc3bc2f9d5 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
@@ -40,7 +40,7 @@ class HomeDrawerFragment @Inject constructor(
         if (savedInstanceState == null) {
             replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
         }
-        session.liveUser(session.myUserId).observeK(this) { optionalUser ->
+        session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser ->
             val user = optionalUser?.getOrNull()
             if (user != null) {
                 avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
index 24318bc508..a00ee24b49 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
@@ -24,7 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory
 import com.airbnb.mvrx.ViewModelContext
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.query.QueryStringValue
 import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.group.groupSummaryQueryParams
 import im.vector.matrix.android.api.session.group.model.GroupSummary
 import im.vector.matrix.android.api.session.room.model.Membership
 import im.vector.matrix.rx.rx
@@ -96,6 +98,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
     }
 
     private fun observeGroupSummaries() {
+        val groupSummariesQueryParams = groupSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+            displayName = QueryStringValue.IsNotEmpty
+        }
         Observable.combineLatest, List>(
                 session
                         .rx()
@@ -109,9 +115,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
                         },
                 session
                         .rx()
-                        .liveGroupSummaries()
-                        // Keep only joined groups. Group invitations will be managed later
-                        .map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } },
+                        .liveGroupSummaries(groupSummariesQueryParams),
                 BiFunction { allCommunityGroup, communityGroups ->
                     listOf(allCommunityGroup) + communityGroups
                 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
index 609e7e2183..7ca647ea3e 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt
@@ -24,9 +24,11 @@ import android.widget.EditText
 import com.otaliastudios.autocomplete.Autocomplete
 import com.otaliastudios.autocomplete.AutocompleteCallback
 import com.otaliastudios.autocomplete.CharPolicy
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.session.group.model.GroupSummary
+import im.vector.matrix.android.api.session.room.model.RoomMember
 import im.vector.matrix.android.api.session.room.model.RoomSummary
-import im.vector.matrix.android.api.session.user.model.User
 import im.vector.matrix.android.api.util.MatrixItem
 import im.vector.matrix.android.api.util.toMatrixItem
 import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
@@ -36,24 +38,29 @@ import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresente
 import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
 import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter
 import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
+import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter
 import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
-import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
 import im.vector.riotx.features.command.Command
 import im.vector.riotx.features.home.AvatarRenderer
-import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
 import im.vector.riotx.features.html.PillImageSpan
 import im.vector.riotx.features.themes.ThemeUtils
-import javax.inject.Inject
 
-class AutoCompleter @Inject constructor(
+class AutoCompleter @AssistedInject constructor(
+        @Assisted val roomId: String,
         private val avatarRenderer: AvatarRenderer,
         private val commandAutocompletePolicy: CommandAutocompletePolicy,
         private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
-        private val autocompleteUserPresenter: AutocompleteUserPresenter,
+        private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory,
         private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
         private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
         private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
 ) {
+
+    @AssistedInject.Factory
+    interface Factory {
+        fun create(roomId: String): AutoCompleter
+    }
+
     private lateinit var editText: EditText
 
     fun enterSpecialMode() {
@@ -68,22 +75,14 @@ class AutoCompleter @Inject constructor(
         GlideApp.with(editText)
     }
 
-    fun setup(editText: EditText, listener: AutoCompleterListener) {
+    fun setup(editText: EditText) {
         this.editText = editText
-
         val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
-
         setupCommands(backgroundDrawable, editText)
-        setupUsers(backgroundDrawable, editText, listener)
-        setupRooms(backgroundDrawable, editText, listener)
-        setupGroups(backgroundDrawable, editText, listener)
+        setupMembers(backgroundDrawable, editText)
+        setupGroups(backgroundDrawable, editText)
         setupEmojis(backgroundDrawable, editText)
-    }
-
-    fun render(state: TextComposerViewState) {
-        autocompleteUserPresenter.render(state.asyncUsers)
-        autocompleteRoomPresenter.render(state.asyncRooms)
-        autocompleteGroupPresenter.render(state.asyncGroups)
+        setupRooms(backgroundDrawable, editText)
     }
 
     private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
@@ -107,15 +106,15 @@ class AutoCompleter @Inject constructor(
                 .build()
     }
 
-    private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) {
-        autocompleteUserPresenter.callback = listener
-        Autocomplete.on(editText)
+    private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
+        val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
+        Autocomplete.on(editText)
                 .with(CharPolicy('@', true))
-                .with(autocompleteUserPresenter)
+                .with(autocompleteMemberPresenter)
                 .with(ELEVATION)
                 .with(backgroundDrawable)
-                .with(object : AutocompleteCallback {
-                    override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
+                .with(object : AutocompleteCallback {
+                    override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean {
                         insertMatrixItem(editText, editable, "@", item.toMatrixItem())
                         return true
                     }
@@ -126,8 +125,7 @@ class AutoCompleter @Inject constructor(
                 .build()
     }
 
-    private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) {
-        autocompleteRoomPresenter.callback = listener
+    private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
         Autocomplete.on(editText)
                 .with(CharPolicy('#', true))
                 .with(autocompleteRoomPresenter)
@@ -145,8 +143,7 @@ class AutoCompleter @Inject constructor(
                 .build()
     }
 
-    private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) {
-        autocompleteGroupPresenter.callback = listener
+    private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
         Autocomplete.on(editText)
                 .with(CharPolicy('+', true))
                 .with(autocompleteGroupPresenter)
@@ -226,11 +223,6 @@ class AutoCompleter @Inject constructor(
         editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
     }
 
-    interface AutoCompleterListener :
-            AutocompleteUserPresenter.Callback,
-            AutocompleteRoomPresenter.Callback,
-            AutocompleteGroupPresenter.Callback
-
     companion object {
         private const val ELEVATION = 6f
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index a956d0e2e9..daf7755fc4 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -78,10 +78,7 @@ import im.vector.riotx.features.attachments.ContactAttachment
 import im.vector.riotx.features.command.Command
 import im.vector.riotx.features.home.AvatarRenderer
 import im.vector.riotx.features.home.getColorFromUserId
-import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
 import im.vector.riotx.features.home.room.detail.composer.TextComposerView
-import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
-import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
 import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
 import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
 import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction
@@ -127,17 +124,15 @@ class RoomDetailFragment @Inject constructor(
         private val session: Session,
         private val avatarRenderer: AvatarRenderer,
         private val timelineEventController: TimelineEventController,
-        private val autoCompleter: AutoCompleter,
+        autoCompleterFactory: AutoCompleter.Factory,
         private val permalinkHandler: PermalinkHandler,
         private val notificationDrawerManager: NotificationDrawerManager,
         val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
-        val textComposerViewModelFactory: TextComposerViewModel.Factory,
         private val eventHtmlRenderer: EventHtmlRenderer,
         private val vectorPreferences: VectorPreferences
 ) :
         VectorBaseFragment(),
         TimelineEventController.Callback,
-        AutoCompleter.AutoCompleterListener,
         VectorInviteView.Callback,
         JumpToReadMarkerView.Callback,
         AttachmentTypeSelectorView.Callback,
@@ -167,9 +162,10 @@ class RoomDetailFragment @Inject constructor(
         GlideApp.with(this)
     }
 
+    private val autoCompleter: AutoCompleter by lazy {
+        autoCompleterFactory.create(roomDetailArgs.roomId)
+    }
     private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
-    private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
-
     private val debouncer = Debouncer(createUIHandler())
 
     private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@@ -205,9 +201,9 @@ class RoomDetailFragment @Inject constructor(
         setupNotificationView()
         setupJumpToReadMarkerView()
         setupJumpToBottomView()
+
         roomDetailViewModel.subscribe { renderState(it) }
-        textComposerViewModel.subscribe { renderTextComposerState(it) }
-        roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
+        roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) }
 
         roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair ->
             val message = requireContext().getString(pair.first, *pair.second.toTypedArray())
@@ -250,9 +246,9 @@ class RoomDetailFragment @Inject constructor(
         roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
             when (mode) {
                 is SendMode.REGULAR -> renderRegularMode(mode.text)
-                is SendMode.EDIT    -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
-                is SendMode.QUOTE   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
-                is SendMode.REPLY   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
+                is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
+                is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
+                is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
             }
         }
 
@@ -279,9 +275,9 @@ class RoomDetailFragment @Inject constructor(
         super.onActivityCreated(savedInstanceState)
         if (savedInstanceState == null) {
             when (val sharedData = roomDetailArgs.sharedData) {
-                is SharedData.Text        -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
+                is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
                 is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
-                null                      -> Timber.v("No share data to process")
+                null -> Timber.v("No share data to process")
             }
         }
     }
@@ -305,12 +301,10 @@ class RoomDetailFragment @Inject constructor(
         jumpToBottomView.setOnClickListener {
             roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
             jumpToBottomView.visibility = View.INVISIBLE
-            withState(roomDetailViewModel) { state ->
-                if (state.timeline?.isLive == false) {
-                    state.timeline.restartWithEventId(null)
-                } else {
-                    layoutManager.scrollToPosition(0)
-                }
+            if (!roomDetailViewModel.timeline.isLive) {
+                roomDetailViewModel.timeline.restartWithEventId(null)
+            } else {
+                layoutManager.scrollToPosition(0)
             }
         }
 
@@ -418,7 +412,8 @@ class RoomDetailFragment @Inject constructor(
         composerLayout.sendButton.setContentDescription(getString(descriptionRes))
 
         avatarRenderer.render(
-                MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
+                MatrixItem.UserItem(event.root.senderId
+                        ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
                 composerLayout.composerRelatedMessageAvatar
         )
         composerLayout.expand {
@@ -468,6 +463,9 @@ class RoomDetailFragment @Inject constructor(
 // PRIVATE METHODS *****************************************************************************
 
     private fun setupRecyclerView() {
+        timelineEventController.callback = this
+        timelineEventController.timeline = roomDetailViewModel.timeline
+
         val epoxyVisibilityTracker = EpoxyVisibilityTracker()
         epoxyVisibilityTracker.attach(recyclerView)
         layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
@@ -487,8 +485,6 @@ class RoomDetailFragment @Inject constructor(
         timelineEventController.addModelBuildListener(modelBuildListener)
         recyclerView.adapter = timelineEventController.adapter
 
-        timelineEventController.callback = this
-
         if (vectorPreferences.swipeToReplyIsEnabled()) {
             val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
                 override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
@@ -505,7 +501,7 @@ class RoomDetailFragment @Inject constructor(
                         is MessageTextItem -> {
                             return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
                         }
-                        else               -> false
+                        else -> false
                     }
                 }
             }
@@ -520,9 +516,9 @@ class RoomDetailFragment @Inject constructor(
             withState(roomDetailViewModel) {
                 val showJumpToUnreadBanner = when (it.unreadState) {
                     UnreadState.Unknown,
-                    UnreadState.HasNoUnread            -> false
+                    UnreadState.HasNoUnread -> false
                     is UnreadState.ReadMarkerNotLoaded -> true
-                    is UnreadState.HasUnread           -> {
+                    is UnreadState.HasUnread -> {
                         if (it.canShowJumpToReadMarker) {
                             val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
                             val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
@@ -536,14 +532,13 @@ class RoomDetailFragment @Inject constructor(
                         }
                     }
                 }
-                jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
+                jumpToReadMarkerView?.isVisible = showJumpToUnreadBanner
             }
         }
     }
 
     private fun setupComposer() {
-        autoCompleter.setup(composerLayout.composerEditText, this)
-
+        autoCompleter.setup(composerLayout.composerEditText)
         composerLayout.callback = object : TextComposerView.Callback {
             override fun onAddAttachment() {
                 if (!::attachmentTypeSelector.isInitialized) {
@@ -598,7 +593,7 @@ class RoomDetailFragment @Inject constructor(
         val summary = state.asyncRoomSummary()
         val inviter = state.asyncInviter()
         if (summary?.membership == Membership.JOIN) {
-            scrollOnHighlightedEventCallback.timeline = state.timeline
+            scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
             timelineEventController.update(state)
             inviteView.visibility = View.GONE
             val uid = session.myUserId
@@ -613,9 +608,10 @@ class RoomDetailFragment @Inject constructor(
         } else if (state.asyncInviter.complete) {
             vectorBaseActivity.finish()
         }
+        val isRoomEncrypted = summary?.isEncrypted ?: false
         if (state.tombstoneEvent == null) {
             composerLayout.visibility = View.VISIBLE
-            composerLayout.setRoomEncrypted(state.isEncrypted)
+            composerLayout.setRoomEncrypted(isRoomEncrypted)
             notificationAreaView.render(NotificationAreaView.State.Hidden)
         } else {
             composerLayout.visibility = View.GONE
@@ -638,10 +634,6 @@ class RoomDetailFragment @Inject constructor(
         }
     }
 
-    private fun renderTextComposerState(state: TextComposerViewState) {
-        autoCompleter.render(state)
-    }
-
     private fun renderTombstoneEventHandling(async: Async) {
         when (async) {
             is Loading -> {
@@ -654,7 +646,7 @@ class RoomDetailFragment @Inject constructor(
                 navigator.openRoom(vectorBaseActivity, async())
                 vectorBaseActivity.finish()
             }
-            is Fail    -> {
+            is Fail -> {
                 vectorBaseActivity.hideWaitingView()
                 vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
             }
@@ -663,23 +655,23 @@ class RoomDetailFragment @Inject constructor(
 
     private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
         when (sendMessageResult) {
-            is SendMessageResult.MessageSent                -> {
+            is SendMessageResult.MessageSent -> {
                 updateComposerText("")
             }
-            is SendMessageResult.SlashCommandHandled        -> {
+            is SendMessageResult.SlashCommandHandled -> {
                 sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
                 updateComposerText("")
             }
-            is SendMessageResult.SlashCommandError          -> {
+            is SendMessageResult.SlashCommandError -> {
                 displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
             }
-            is SendMessageResult.SlashCommandUnknown        -> {
+            is SendMessageResult.SlashCommandUnknown -> {
                 displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
             }
-            is SendMessageResult.SlashCommandResultOk       -> {
+            is SendMessageResult.SlashCommandResultOk -> {
                 updateComposerText("")
             }
-            is SendMessageResult.SlashCommandResultError    -> {
+            is SendMessageResult.SlashCommandResultError -> {
                 displayCommandError(sendMessageResult.throwable.localizedMessage)
             }
             is SendMessageResult.SlashCommandNotImplemented -> {
@@ -717,7 +709,7 @@ class RoomDetailFragment @Inject constructor(
 
     private fun displayRoomDetailActionResult(result: Async) {
         when (result) {
-            is Fail    -> {
+            is Fail -> {
                 AlertDialog.Builder(requireActivity())
                         .setTitle(R.string.dialog_title_error)
                         .setMessage(errorFormatter.toHumanReadable(result.error))
@@ -728,7 +720,7 @@ class RoomDetailFragment @Inject constructor(
                 when (val data = result.invoke()) {
                     is RoomDetailAction.ReportContent -> {
                         when {
-                            data.spam          -> {
+                            data.spam -> {
                                 AlertDialog.Builder(requireActivity())
                                         .setTitle(R.string.content_reported_as_spam_title)
                                         .setMessage(R.string.content_reported_as_spam_content)
@@ -750,7 +742,7 @@ class RoomDetailFragment @Inject constructor(
                                         .show()
                                         .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
                             }
-                            else               -> {
+                            else -> {
                                 AlertDialog.Builder(requireActivity())
                                         .setTitle(R.string.content_reported_title)
                                         .setMessage(R.string.content_reported_content)
@@ -863,14 +855,14 @@ class RoomDetailFragment @Inject constructor(
     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
         if (allGranted(grantResults)) {
             when (requestCode) {
-                PERMISSION_REQUEST_CODE_DOWNLOAD_FILE   -> {
+                PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
                     val action = roomDetailViewModel.pendingAction
                     if (action != null) {
                         roomDetailViewModel.pendingAction = null
                         roomDetailViewModel.handle(action)
                     }
                 }
-                PERMISSION_REQUEST_CODE_INCOMING_URI    -> {
+                PERMISSION_REQUEST_CODE_INCOMING_URI -> {
                     val pendingUri = roomDetailViewModel.pendingUri
                     if (pendingUri != null) {
                         roomDetailViewModel.pendingUri = null
@@ -966,43 +958,25 @@ class RoomDetailFragment @Inject constructor(
         roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
     }
 
-    // AutocompleteUserPresenter.Callback
-
-    override fun onQueryUsers(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryUsers(query))
-    }
-
-    // AutocompleteRoomPresenter.Callback
-
-    override fun onQueryRooms(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryRooms(query))
-    }
-
-    // AutocompleteGroupPresenter.Callback
-
-    override fun onQueryGroups(query: CharSequence?) {
-        textComposerViewModel.handle(TextComposerAction.QueryGroups(query))
-    }
-
     private fun handleActions(action: EventSharedAction) {
         when (action) {
-            is EventSharedAction.AddReaction                -> {
+            is EventSharedAction.AddReaction -> {
                 startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
             }
-            is EventSharedAction.ViewReactions              -> {
+            is EventSharedAction.ViewReactions -> {
                 ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
                         .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
             }
-            is EventSharedAction.Copy                       -> {
+            is EventSharedAction.Copy -> {
                 // I need info about the current selected message :/
                 copyToClipboard(requireContext(), action.content, false)
                 val msg = requireContext().getString(R.string.copied_to_clipboard)
                 showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
             }
-            is EventSharedAction.Delete                     -> {
+            is EventSharedAction.Delete -> {
                 roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
             }
-            is EventSharedAction.Share                      -> {
+            is EventSharedAction.Share -> {
                 // TODO current data communication is too limited
                 // Need to now the media type
                 // TODO bad, just POC
@@ -1030,10 +1004,10 @@ class RoomDetailFragment @Inject constructor(
                         }
                 )
             }
-            is EventSharedAction.ViewEditHistory            -> {
+            is EventSharedAction.ViewEditHistory -> {
                 onEditedDecorationClicked(action.messageInformationData)
             }
-            is EventSharedAction.ViewSource                 -> {
+            is EventSharedAction.ViewSource -> {
                 val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
                 view.findViewById(R.id.event_content_text_view)?.let {
                     it.text = action.content
@@ -1044,7 +1018,7 @@ class RoomDetailFragment @Inject constructor(
                         .setPositiveButton(R.string.ok, null)
                         .show()
             }
-            is EventSharedAction.ViewDecryptedSource        -> {
+            is EventSharedAction.ViewDecryptedSource -> {
                 val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
                 view.findViewById(R.id.event_content_text_view)?.let {
                     it.text = action.content
@@ -1055,31 +1029,31 @@ class RoomDetailFragment @Inject constructor(
                         .setPositiveButton(R.string.ok, null)
                         .show()
             }
-            is EventSharedAction.QuickReact                 -> {
+            is EventSharedAction.QuickReact -> {
                 // eventId,ClickedOn,Add
                 roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
             }
-            is EventSharedAction.Edit                       -> {
+            is EventSharedAction.Edit -> {
                 roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString()))
             }
-            is EventSharedAction.Quote                      -> {
+            is EventSharedAction.Quote -> {
                 roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString()))
             }
-            is EventSharedAction.Reply                      -> {
+            is EventSharedAction.Reply -> {
                 roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString()))
             }
-            is EventSharedAction.CopyPermalink              -> {
+            is EventSharedAction.CopyPermalink -> {
                 val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
                 copyToClipboard(requireContext(), permalink, false)
                 showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
             }
-            is EventSharedAction.Resend                     -> {
+            is EventSharedAction.Resend -> {
                 roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
             }
-            is EventSharedAction.Remove                     -> {
+            is EventSharedAction.Remove -> {
                 roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
             }
-            is EventSharedAction.ReportContentSpam          -> {
+            is EventSharedAction.ReportContentSpam -> {
                 roomDetailViewModel.handle(RoomDetailAction.ReportContent(
                         action.eventId, action.senderId, "This message is spam", spam = true))
             }
@@ -1087,19 +1061,19 @@ class RoomDetailFragment @Inject constructor(
                 roomDetailViewModel.handle(RoomDetailAction.ReportContent(
                         action.eventId, action.senderId, "This message is inappropriate", inappropriate = true))
             }
-            is EventSharedAction.ReportContentCustom        -> {
+            is EventSharedAction.ReportContentCustom -> {
                 promptReasonToReportContent(action)
             }
-            is EventSharedAction.IgnoreUser                 -> {
+            is EventSharedAction.IgnoreUser -> {
                 roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
             }
-            is EventSharedAction.OnUrlClicked               -> {
+            is EventSharedAction.OnUrlClicked -> {
                 onUrlClicked(action.url)
             }
-            is EventSharedAction.OnUrlLongClicked           -> {
+            is EventSharedAction.OnUrlLongClicked -> {
                 onUrlLongClicked(action.url)
             }
-            else                                            -> {
+            else -> {
                 Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
             }
         }
@@ -1207,10 +1181,10 @@ class RoomDetailFragment @Inject constructor(
 
     private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
         when (type) {
-            AttachmentTypeSelectorView.Type.CAMERA  -> attachmentsHelper.openCamera()
-            AttachmentTypeSelectorView.Type.FILE    -> attachmentsHelper.selectFile()
+            AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
+            AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
             AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
-            AttachmentTypeSelectorView.Type.AUDIO   -> attachmentsHelper.selectAudio()
+            AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
             AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
             AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
         }
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 467148302f..c93358a04e 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
@@ -20,14 +20,18 @@ import android.net.Uri
 import androidx.annotation.IdRes
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
-import com.airbnb.mvrx.*
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.ViewModelContext
 import com.jakewharton.rxrelay2.BehaviorRelay
 import com.jakewharton.rxrelay2.PublishRelay
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.MatrixPatterns
-import im.vector.matrix.android.api.failure.Failure
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.events.model.EventType
 import im.vector.matrix.android.api.session.events.model.isImageMessage
@@ -89,20 +93,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     private val visibleEventsObservable = BehaviorRelay.create()
     private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
         TimelineSettings(30,
-                filterEdits = false,
-                filterTypes = true,
-                allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
-                buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
+                         filterEdits = false,
+                         filterTypes = true,
+                         allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
+                         buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
     } else {
         TimelineSettings(30,
-                filterEdits = true,
-                filterTypes = true,
-                allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
-                buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
+                         filterEdits = true,
+                         filterTypes = true,
+                         allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
+                         buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
     }
 
     private var timelineEvents = PublishRelay.create>()
-    private var timeline = room.createTimeline(eventId, timelineSettings)
+    var timeline = room.createTimeline(eventId, timelineSettings)
+        private set
 
     private val _viewEvents = PublishDataSource()
     val viewEvents: DataSource = _viewEvents
@@ -138,18 +143,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     init {
+        timeline.start()
+        timeline.addListener(this)
+        observeRoomSummary()
+        observeSummaryState()
         getUnreadState()
         observeSyncState()
-        observeRoomSummary()
         observeEventDisplayedActions()
-        observeSummaryState()
         observeDrafts()
         observeUnreadState()
+        room.getRoomSummaryLive()
         room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
-        timeline.addListener(this)
-        timeline.start()
-        setState { copy(timeline = this@RoomDetailViewModel.timeline) }
-
         // Inform the SDK that the room is displayed
         session.onRoomDisplayed(initialState.roomId)
     }
@@ -233,23 +237,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                         copy(
                                 // Create a sendMode from a draft and retrieve the TimelineEvent
                                 sendMode = when (draft) {
-                                    is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
-                                    is UserDraft.QUOTE   -> {
-                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
-                                            SendMode.QUOTE(timelineEvent, draft.text)
-                                        }
-                                    }
-                                    is UserDraft.REPLY   -> {
-                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
-                                            SendMode.REPLY(timelineEvent, draft.text)
-                                        }
-                                    }
-                                    is UserDraft.EDIT    -> {
-                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
-                                            SendMode.EDIT(timelineEvent, draft.text)
-                                        }
-                                    }
-                                } ?: SendMode.REGULAR("")
+                                               is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
+                                               is UserDraft.QUOTE   -> {
+                                                   room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                                       SendMode.QUOTE(timelineEvent, draft.text)
+                                                   }
+                                               }
+                                               is UserDraft.REPLY   -> {
+                                                   room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                                       SendMode.REPLY(timelineEvent, draft.text)
+                                                   }
+                                               }
+                                               is UserDraft.EDIT    -> {
+                                                   room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                                       SendMode.EDIT(timelineEvent, draft.text)
+                                                   }
+                                               }
+                                           } ?: SendMode.REGULAR("")
                         )
                     }
                 }
@@ -258,7 +262,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
         val tombstoneContent = action.event.getClearContent().toModel()
-                ?: return
+                               ?: return
 
         val roomId = tombstoneContent.replacementRoom ?: ""
         val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
@@ -310,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         else                     -> false
     }
 
-    // PRIVATE METHODS *****************************************************************************
+// PRIVATE METHODS *****************************************************************************
 
     private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
         withState { state ->
@@ -396,7 +400,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is SendMode.EDIT    -> {
                     // is original event a reply?
                     val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId
-                            ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId
+                                    ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId
                     if (inReplyTo != null) {
                         // TODO check if same content?
                         room.getTimeLineEvent(inReplyTo)?.let {
@@ -405,13 +409,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                     } else {
                         val messageContent: MessageContent? =
                                 state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
-                                        ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
+                                ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
                         val existingBody = messageContent?.body ?: ""
                         if (existingBody != action.text) {
                             room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
-                                    messageContent?.type ?: MessageType.MSGTYPE_TEXT,
-                                    action.text,
-                                    action.autoMarkdown)
+                                                 messageContent?.type ?: MessageType.MSGTYPE_TEXT,
+                                                 action.text,
+                                                 action.autoMarkdown)
                         } else {
                             Timber.w("Same message content, do not send edition")
                         }
@@ -422,7 +426,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is SendMode.QUOTE   -> {
                     val messageContent: MessageContent? =
                             state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
-                                    ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
+                            ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
                     val textMsg = messageContent?.body
 
                     val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
@@ -538,7 +542,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
                 null -> room.sendMedias(attachments)
                 else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
-                        ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
+                                                                             ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
             }
         }
     }
@@ -728,7 +732,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 .filter { it.isNotEmpty() }
                 .subscribeBy(onNext = { actions ->
                     val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
-                            ?: return@subscribeBy
+                                                           ?: return@subscribeBy
                     val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
                     if (trackUnreadMessages.get()) {
                         if (globalMostRecentDisplayedEvent == null) {
@@ -791,10 +795,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         room.rx().liveRoomSummary()
                 .unwrap()
                 .execute { async ->
-                    copy(
-                            asyncRoomSummary = async,
-                            isEncrypted = room.isEncrypted()
-                    )
+                    copy(asyncRoomSummary = async)
                 }
     }
 
@@ -880,7 +881,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     override fun onCleared() {
         timeline.dispose()
-        timeline.removeListener(this)
+        timeline.removeAllListeners()
         super.onCleared()
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
index b2ad29668e..165ef7b625 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
@@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
 import com.airbnb.mvrx.Uninitialized
 import im.vector.matrix.android.api.session.events.model.Event
 import im.vector.matrix.android.api.session.room.model.RoomSummary
-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.sync.SyncState
 import im.vector.matrix.android.api.session.user.model.User
@@ -51,11 +50,9 @@ sealed class UnreadState {
 data class RoomDetailViewState(
         val roomId: String,
         val eventId: String?,
-        val timeline: Timeline? = null,
         val asyncInviter: Async = Uninitialized,
         val asyncRoomSummary: Async = Uninitialized,
         val sendMode: SendMode = SendMode.REGULAR(""),
-        val isEncrypted: Boolean = false,
         val tombstoneEvent: Event? = null,
         val tombstoneEventHandling: Async = Uninitialized,
         val syncState: SyncState = SyncState.Idle,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt
deleted file mode 100644
index f7ec78c6c4..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewModel.kt
+++ /dev/null
@@ -1,158 +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.riotx.features.home.room.detail.composer
-
-import arrow.core.Option
-import com.airbnb.mvrx.FragmentViewModelContext
-import com.airbnb.mvrx.MvRxViewModelFactory
-import com.airbnb.mvrx.ViewModelContext
-import com.jakewharton.rxrelay2.BehaviorRelay
-import com.squareup.inject.assisted.Assisted
-import com.squareup.inject.assisted.AssistedInject
-import im.vector.matrix.android.api.session.Session
-import im.vector.matrix.android.api.session.group.model.GroupSummary
-import im.vector.matrix.android.api.session.room.model.RoomSummary
-import im.vector.matrix.android.api.session.user.model.User
-import im.vector.matrix.rx.rx
-import im.vector.riotx.core.platform.VectorViewModel
-import im.vector.riotx.features.home.room.detail.RoomDetailFragment
-import io.reactivex.Observable
-import io.reactivex.functions.BiFunction
-import java.util.concurrent.TimeUnit
-
-typealias AutocompleteQuery = CharSequence
-
-class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState,
-                                                        private val session: Session
-) : VectorViewModel(initialState) {
-
-    private val room = session.getRoom(initialState.roomId)!!
-
-    private val usersQueryObservable = BehaviorRelay.create>()
-    private val roomsQueryObservable = BehaviorRelay.create>()
-    private val groupsQueryObservable = BehaviorRelay.create>()
-
-    @AssistedInject.Factory
-    interface Factory {
-        fun create(initialState: TextComposerViewState): TextComposerViewModel
-    }
-
-    companion object : MvRxViewModelFactory {
-
-        @JvmStatic
-        override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? {
-            val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
-            return fragment.textComposerViewModelFactory.create(state)
-        }
-    }
-
-    init {
-        observeUsersQuery()
-        observeRoomsQuery()
-        observeGroupsQuery()
-    }
-
-    override fun handle(action: TextComposerAction) {
-        when (action) {
-            is TextComposerAction.QueryUsers  -> handleQueryUsers(action)
-            is TextComposerAction.QueryRooms  -> handleQueryRooms(action)
-            is TextComposerAction.QueryGroups -> handleQueryGroups(action)
-        }
-    }
-
-    private fun handleQueryUsers(action: TextComposerAction.QueryUsers) {
-        val query = Option.fromNullable(action.query)
-        usersQueryObservable.accept(query)
-    }
-
-    private fun handleQueryRooms(action: TextComposerAction.QueryRooms) {
-        val query = Option.fromNullable(action.query)
-        roomsQueryObservable.accept(query)
-    }
-
-    private fun handleQueryGroups(action: TextComposerAction.QueryGroups) {
-        val query = Option.fromNullable(action.query)
-        groupsQueryObservable.accept(query)
-    }
-
-    private fun observeUsersQuery() {
-        Observable.combineLatest, Option, List>(
-                room.rx().liveRoomMemberIds(),
-                usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
-                BiFunction { roomMemberIds, query ->
-                    val users = roomMemberIds.mapNotNull { session.getUser(it) }
-
-                    val filter = query.orNull()
-                    if (filter.isNullOrBlank()) {
-                        users
-                    } else {
-                        users.filter {
-                            it.displayName?.contains(filter, ignoreCase = true) ?: false
-                        }
-                    }
-                            .sortedBy { it.displayName }
-                }
-        ).execute { async ->
-            copy(
-                    asyncUsers = async
-            )
-        }
-    }
-
-    private fun observeRoomsQuery() {
-        Observable.combineLatest, Option, List>(
-                session.rx().liveRoomSummaries(),
-                roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
-                BiFunction { roomSummaries, query ->
-                    val filter = query.orNull() ?: ""
-                    // Keep only room with a canonical alias
-                    roomSummaries
-                            .filter {
-                                it.canonicalAlias?.contains(filter, ignoreCase = true) == true
-                            }
-                            .sortedBy { it.displayName }
-                }
-        ).execute { async ->
-            copy(
-                    asyncRooms = async
-            )
-        }
-    }
-
-    private fun observeGroupsQuery() {
-        Observable.combineLatest, Option, List>(
-                session.rx().liveGroupSummaries(),
-                groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
-                BiFunction { groupSummaries, query ->
-                    val filter = query.orNull()
-                    if (filter.isNullOrBlank()) {
-                        groupSummaries
-                    } else {
-                        groupSummaries
-                                .filter {
-                                    it.groupId.contains(filter, ignoreCase = true)
-                                }
-                    }
-                            .sortedBy { it.displayName }
-                }
-        ).execute { async ->
-            copy(
-                    asyncGroups = async
-            )
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt
deleted file mode 100644
index e863970afe..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerViewState.kt
+++ /dev/null
@@ -1,34 +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.riotx.features.home.room.detail.composer
-
-import com.airbnb.mvrx.Async
-import com.airbnb.mvrx.MvRxState
-import com.airbnb.mvrx.Uninitialized
-import im.vector.matrix.android.api.session.group.model.GroupSummary
-import im.vector.matrix.android.api.session.room.model.RoomSummary
-import im.vector.matrix.android.api.session.user.model.User
-import im.vector.riotx.features.home.room.detail.RoomDetailArgs
-
-data class TextComposerViewState(val roomId: String,
-                                 val asyncUsers: Async> = Uninitialized,
-                                 val asyncRooms: Async> = Uninitialized,
-                                 val asyncGroups: Async> = Uninitialized
-) : MvRxState {
-
-    constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
-}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
index 582544ce8a..a08669da3b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
@@ -95,12 +95,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     private val modelCache = arrayListOf()
     private var currentSnapshot: List = emptyList()
     private var inSubmitList: Boolean = false
-    private var timeline: Timeline? = null
     private var unreadState: UnreadState = UnreadState.Unknown
     private var positionOfReadMarker: Int? = null
     private var eventIdToHighlight: String? = null
 
     var callback: Callback? = null
+    var timeline: Timeline? = null
 
     private val listUpdateCallback = object : ListUpdateCallback {
 
@@ -176,10 +176,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     }
 
     fun update(viewState: RoomDetailViewState) {
-        if (timeline?.timelineID != viewState.timeline?.timelineID) {
-            timeline = viewState.timeline
-            timeline?.addListener(this)
-        }
         var requestModelBuild = false
         if (eventIdToHighlight != viewState.highlightedEventId) {
             // Clear cache to force a refresh
@@ -205,6 +201,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
 
     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
         super.onAttachedToRecyclerView(recyclerView)
+        timeline?.addListener(this)
         timelineMediaSizeProvider.recyclerView = recyclerView
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index 939564e780..9a2fb4b6de 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
@@ -93,7 +93,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
         }
 
         // Action
-        state.actions()?.forEachIndexed { index, action ->
+        state.actions.forEachIndexed { index, action ->
             if (action is EventSharedAction.Separator) {
                 bottomSheetSeparatorItem {
                     id("separator_$index")
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 aad73e12f4..3f0e8b041f 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
@@ -31,8 +31,7 @@ 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.getLastMessageContent
 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.matrix.rx.rx
 import im.vector.matrix.rx.unwrap
 import im.vector.riotx.R
 import im.vector.riotx.core.extensions.canReact
@@ -42,8 +41,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
 import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
 import im.vector.riotx.features.html.EventHtmlRenderer
 import im.vector.riotx.features.html.VectorHtmlCompressor
-import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.reactions.data.EmojiDataSource
+import im.vector.riotx.features.settings.VectorPreferences
 import java.text.SimpleDateFormat
 import java.util.*
 
@@ -64,7 +63,7 @@ data class MessageActionState(
         // For quick reactions
         val quickStates: Async> = Uninitialized,
         // For actions
-        val actions: Async> = Uninitialized,
+        val actions: List = emptyList(),
         val expendedReportContentMenu: Boolean = false
 ) : MvRxState {
 
@@ -112,7 +111,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     init {
         observeEvent()
         observeReactions()
-        observeEventAction()
+        observeTimelineEventState()
     }
 
     override fun handle(action: MessageActionsAction) {
@@ -131,32 +130,17 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
 
     private fun observeEvent() {
         if (room == null) return
-        RxRoom(room)
+        room.rx()
                 .liveTimelineEvent(eventId)
                 .unwrap()
                 .execute {
-                    copy(
-                            timelineEvent = it,
-                            messageBody = computeMessageBody(it)
-                    )
-                }
-    }
-
-    private fun observeEventAction() {
-        if (room == null) return
-        RxRoom(room)
-                .liveTimelineEvent(eventId)
-                .map {
-                    actionsForEvent(it)
-                }
-                .execute {
-                    copy(actions = it)
+                    copy(timelineEvent = it)
                 }
     }
 
     private fun observeReactions() {
         if (room == null) return
-        RxRoom(room)
+        room.rx()
                 .liveAnnotationSummary(eventId)
                 .map { annotations ->
                     EmojiDataSource.quickEmojis.map { emoji ->
@@ -168,11 +152,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                 }
     }
 
-    private fun computeMessageBody(timelineEvent: Async): CharSequence? {
-        return when (timelineEvent()?.root?.getClearType()) {
+    private fun observeTimelineEventState() {
+        asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent ->
+            val computedMessage = computeMessageBody(timelineEvent)
+            val actions = actionsForEvent(timelineEvent)
+            setState { copy(messageBody = computedMessage, actions = actions) }
+        }
+    }
+
+    private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence? {
+        return when (timelineEvent.root.getClearType()) {
             EventType.MESSAGE,
             EventType.STICKER     -> {
-                val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
+                val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
                 if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
                     val html = messageContent.formattedBody
                             ?.takeIf { it.isNotBlank() }
@@ -193,41 +185,39 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.CALL_ANSWER -> {
-                timelineEvent()?.let { noticeEventFormatter.format(it) }
+                noticeEventFormatter.format(timelineEvent)
             }
             else                  -> null
         }
     }
 
-    private fun actionsForEvent(optionalEvent: Optional): List {
-        val event = optionalEvent.getOrNull() ?: return emptyList()
-
-        val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
-                ?: event.root.getClearContent().toModel()
+    private fun actionsForEvent(timelineEvent: TimelineEvent): List {
+        val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
+                ?: timelineEvent.root.getClearContent().toModel()
         val type = messageContent?.type
 
         return arrayListOf().apply {
-            if (event.root.sendState.hasFailed()) {
-                if (canRetry(event)) {
+            if (timelineEvent.root.sendState.hasFailed()) {
+                if (canRetry(timelineEvent)) {
                     add(EventSharedAction.Resend(eventId))
                 }
                 add(EventSharedAction.Remove(eventId))
-            } else if (event.root.sendState.isSending()) {
+            } else if (timelineEvent.root.sendState.isSending()) {
                 // TODO is uploading attachment?
-                if (canCancel(event)) {
+                if (canCancel(timelineEvent)) {
                     add(EventSharedAction.Cancel(eventId))
                 }
-            } else if (event.root.sendState == SendState.SYNCED) {
-                if (!event.root.isRedacted()) {
-                    if (canReply(event, messageContent)) {
+            } else if (timelineEvent.root.sendState == SendState.SYNCED) {
+                if (!timelineEvent.root.isRedacted()) {
+                    if (canReply(timelineEvent, messageContent)) {
                         add(EventSharedAction.Reply(eventId))
                     }
 
-                    if (canEdit(event, session.myUserId)) {
+                    if (canEdit(timelineEvent, session.myUserId)) {
                         add(EventSharedAction.Edit(eventId))
                     }
 
-                    if (canRedact(event, session.myUserId)) {
+                    if (canRedact(timelineEvent, session.myUserId)) {
                         add(EventSharedAction.Delete(eventId))
                     }
 
@@ -236,19 +226,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                         add(EventSharedAction.Copy(messageContent!!.body))
                     }
 
-                    if (event.canReact()) {
+                    if (timelineEvent.canReact()) {
                         add(EventSharedAction.AddReaction(eventId))
                     }
 
-                    if (canQuote(event, messageContent)) {
+                    if (canQuote(timelineEvent, messageContent)) {
                         add(EventSharedAction.Quote(eventId))
                     }
 
-                    if (canViewReactions(event)) {
+                    if (canViewReactions(timelineEvent)) {
                         add(EventSharedAction.ViewReactions(informationData))
                     }
 
-                    if (event.hasBeenEdited()) {
+                    if (timelineEvent.hasBeenEdited()) {
                         add(EventSharedAction.ViewEditHistory(informationData))
                     }
 
@@ -261,7 +251,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                         // TODO
                     }
 
-                    if (event.root.sendState == SendState.SENT) {
+                    if (timelineEvent.root.sendState == SendState.SENT) {
                         // TODO Can be redacted
 
                         // TODO sent by me or sufficient power level
@@ -269,24 +259,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                 }
 
                 if (vectorPreferences.developerMode()) {
-                    add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent()))
-                    if (event.isEncrypted()) {
-                        val decryptedContent = event.root.toClearContentStringWithIndent()
+                    add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
+                    if (timelineEvent.isEncrypted()) {
+                        val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
                                 ?: stringProvider.getString(R.string.encryption_information_decryption_error)
                         add(EventSharedAction.ViewDecryptedSource(decryptedContent))
                     }
                 }
-
                 add(EventSharedAction.CopyPermalink(eventId))
-
-                if (session.myUserId != event.root.senderId) {
+                if (session.myUserId != timelineEvent.root.senderId) {
                     // not sent by me
-                    if (event.root.getClearType() == EventType.MESSAGE) {
-                        add(EventSharedAction.ReportContent(eventId, event.root.senderId))
+                    if (timelineEvent.root.getClearType() == EventType.MESSAGE) {
+                        add(EventSharedAction.ReportContent(eventId, timelineEvent.root.senderId))
                     }
 
                     add(EventSharedAction.Separator)
-                    add(EventSharedAction.IgnoreUser(event.root.senderId))
+                    add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
                 }
             }
         }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index 9cb045c01e..a201890912 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -128,8 +128,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
     }
 
     private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
-        val eventContent: RoomMember? = event.getClearContent().toModel()
-        val prevEventContent: RoomMember? = event.prevContent.toModel()
+        val eventContent: RoomMemberContent? = event.getClearContent().toModel()
+        val prevEventContent: RoomMemberContent? = event.prevContent.toModel()
         val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
         return if (isMembershipEvent) {
             buildMembershipNotice(event, senderName, eventContent, prevEventContent)
@@ -166,7 +166,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
                 ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName)
     }
 
-    private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String {
+    private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String {
         val displayText = StringBuilder()
         // Check display name has been changed
         if (eventContent?.displayName != prevEventContent?.displayName) {
@@ -198,7 +198,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
         return displayText.toString()
     }
 
-    private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
+    private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String? {
         val senderDisplayName = senderName ?: event.senderId ?: ""
         val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
         return when (eventContent?.membership) {
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 5ee0576be7..fabdf22d14 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
@@ -49,7 +49,7 @@ abstract class MessageTextItem : AbsMessageItem() {
         holder.messageView.setOnClickListener(attributes.itemClickListener)
         holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
         if (searchForPills) {
-            message?.findPillsAndProcess { it.bind(holder.messageView) }
+            message?.findPillsAndProcess(coroutineScope) { it.bind(holder.messageView) }
         }
         val textFuture = PrecomputedTextCompat.getTextFuture(
                 message ?: "",
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
index 492248985e..043763fd8e 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/tools/EventRenderingTools.kt
@@ -25,14 +25,11 @@ import im.vector.riotx.core.linkify.VectorLinkify
 import im.vector.riotx.core.utils.isValidUrl
 import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
 import im.vector.riotx.features.html.PillImageSpan
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.*
 import me.saket.bettermovementmethod.BetterLinkMovementMethod
 
-fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) {
-    GlobalScope.launch(Dispatchers.Main) {
+fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) {
+    scope.launch(Dispatchers.Main) {
         withContext(Dispatchers.IO) {
             toSpannable().let { spannable ->
                 spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
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 e272c1423f..122b95aa52 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
@@ -59,7 +59,8 @@ data class RoomListParams(
 class RoomListFragment @Inject constructor(
         private val roomController: RoomSummaryController,
         val roomListViewModelFactory: RoomListViewModel.Factory,
-        private val notificationDrawerManager: NotificationDrawerManager
+        private val notificationDrawerManager: NotificationDrawerManager,
+        private val sharedViewPool: RecyclerView.RecycledViewPool
 
 ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
 
@@ -95,7 +96,6 @@ class RoomListFragment @Inject constructor(
         setupCreateRoomButton()
         setupRecyclerView()
         sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
-
         roomListViewModel.subscribe { renderState(it) }
         roomListViewModel.viewEvents
                 .observe()
@@ -193,6 +193,8 @@ class RoomListFragment @Inject constructor(
         val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
         roomListView.layoutManager = layoutManager
         roomListView.itemAnimator = RoomListAnimator()
+        roomListView.setRecycledViewPool(sharedViewPool)
+        layoutManager.recycleChildrenOnDetach = true
         roomController.listener = this
         modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
         roomController.addModelBuildListener(modelBuildListener)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
index 60a26c8151..5fc33ffbe9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
@@ -46,6 +46,7 @@ data class RoomListActionsArgs(
 class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener {
 
     private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
+    @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool
     @Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory
     @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController
     @Inject lateinit var navigator: Navigator
@@ -70,7 +71,7 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
         sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
-        recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false)
+        recyclerView.configureWith(roomListActionsEpoxyController, viewPool = sharedViewPool, hasFixedSize = false)
         // Disable item animation
         recyclerView.itemAnimator = null
         roomListActionsEpoxyController.listener = this
diff --git a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
index a609541a62..3d3dcbea94 100644
--- a/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
+++ b/vector/src/main/java/im/vector/riotx/features/html/PillImageSpan.kt
@@ -88,25 +88,27 @@ class PillImageSpan(private val glideRequests: GlideRequests,
     }
 
     internal fun updateAvatarDrawable(drawable: Drawable?) {
-        pillDrawable.apply {
-            chipIcon = drawable
-        }
-        tv?.get()?.apply {
-            invalidate()
-        }
+        pillDrawable.chipIcon = drawable
+        tv?.get()?.invalidate()
     }
 
     // Private methods *****************************************************************************
 
     private fun createChipDrawable(): ChipDrawable {
         val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
+        val icon = try {
+            avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
+        } catch (exception: Exception) {
+            avatarRenderer.getPlaceholderDrawable(context, matrixItem)
+        }
+
         return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
             text = matrixItem.getBestName()
             textEndPadding = textPadding
             textStartPadding = textPadding
             setChipMinHeightResource(R.dimen.pill_min_height)
             setChipIconSizeResource(R.dimen.pill_avatar_size)
-            chipIcon = avatarRenderer.getPlaceholderDrawable(context, matrixItem)
+            chipIcon = icon
             setBounds(0, 0, intrinsicWidth, intrinsicHeight)
         }
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt
index e38e7d548a..11d770adc4 100644
--- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt
+++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt
@@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.events.model.Event
 import im.vector.matrix.android.api.session.events.model.EventType
 import im.vector.matrix.android.api.session.events.model.toModel
 import im.vector.matrix.android.api.session.room.model.Membership
-import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomMemberContent
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import im.vector.matrix.android.api.session.room.timeline.getEditedEventId
 import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody
@@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
     }
 
     private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
-        val content = event.content?.toModel() ?: return null
+        val content = event.content?.toModel() ?: return null
         val roomId = event.roomId ?: return null
         val dName = event.senderId?.let { session.getUser(it)?.displayName }
         if (Membership.INVITE == content.membership) {
diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt
index 9317c645c4..1d7338e2a4 100644
--- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt
+++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt
@@ -18,10 +18,10 @@ package im.vector.riotx.features.reactions.data
 import android.content.res.Resources
 import com.squareup.moshi.Moshi
 import im.vector.riotx.R
-import im.vector.riotx.core.di.ScreenScope
 import javax.inject.Inject
+import javax.inject.Singleton
 
-@ScreenScope
+@Singleton
 class EmojiDataSource @Inject constructor(
         resources: Resources
 ) {
@@ -41,6 +41,7 @@ class EmojiDataSource @Inject constructor(
 
         // First add emojis with name matching query, sorted by name
         return (rawData.emojis.values
+                .asSequence()
                 .filter { emojiItem ->
                     emojiItem.name.contains(query, true)
                 }
@@ -55,6 +56,7 @@ class EmojiDataSource @Inject constructor(
                         .sortedBy { it.name })
                 // and ensure they will not be present twice
                 .distinct()
+                .toList()
     }
 
     fun getQuickReactions(): List {
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
index dcd64c6a46..c4a91a520a 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt
@@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
 import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
 import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
 import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.matrix.android.api.util.Cancelable
 import im.vector.matrix.rx.rx
 import im.vector.riotx.core.extensions.postLiveEvent
@@ -79,13 +80,14 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
     }
 
     private fun observeJoinedRooms() {
+        val queryParams = roomSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+        }
         session
                 .rx()
-                .liveRoomSummaries()
+                .liveRoomSummaries(queryParams)
                 .subscribe { list ->
                     val joinedRoomIds = list
-                            // Keep only joined room
-                            ?.filter { it.membership == Membership.JOIN }
                             ?.map { it.roomId }
                             ?.toSet()
                             ?: emptySet()
@@ -106,9 +108,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
     override fun handle(action: RoomDirectoryAction) {
         when (action) {
             is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
-            is RoomDirectoryAction.FilterWith           -> filterWith(action)
-            RoomDirectoryAction.LoadMore                -> loadMore()
-            is RoomDirectoryAction.JoinRoom             -> joinRoom(action)
+            is RoomDirectoryAction.FilterWith -> filterWith(action)
+            RoomDirectoryAction.LoadMore -> loadMore()
+            is RoomDirectoryAction.JoinRoom -> joinRoom(action)
         }
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
index 54c86537d2..3de5cb4334 100644
--- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt
@@ -24,6 +24,7 @@ import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.room.model.Membership
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.matrix.rx.rx
 import im.vector.riotx.core.platform.VectorViewModel
 import im.vector.riotx.features.roomdirectory.JoinState
@@ -53,14 +54,15 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
     }
 
     private fun observeJoinedRooms() {
+        val queryParams = roomSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+        }
         session
                 .rx()
-                .liveRoomSummaries()
+                .liveRoomSummaries(queryParams)
                 .subscribe { list ->
                     withState { state ->
                         val isRoomJoined = list
-                                // Keep only joined room
-                                ?.filter { it.membership == Membership.JOIN }
                                 ?.map { it.roomId }
                                 ?.toList()
                                 ?.contains(state.roomId) == true
diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt
index 72c98cdc45..bfe08a5c52 100644
--- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareViewModel.kt
@@ -22,6 +22,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory
 import com.airbnb.mvrx.ViewModelContext
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
 import im.vector.matrix.rx.rx
 import im.vector.riotx.ActiveSessionDataSource
 import im.vector.riotx.core.platform.EmptyAction
@@ -59,10 +60,11 @@ class IncomingShareViewModel @AssistedInject constructor(@Assisted initialState:
     }
 
     private fun observeRoomSummaries() {
+        val queryParams = roomSummaryQueryParams()
         sessionObservableStore.observe()
                 .observeOn(AndroidSchedulers.mainThread())
                 .switchMap {
-                    it.orNull()?.rx()?.liveRoomSummaries()
+                    it.orNull()?.rx()?.liveRoomSummaries(queryParams)
                             ?: Observable.just(emptyList())
                 }
                 .throttleLast(300, TimeUnit.MILLISECONDS)
diff --git a/vector/src/main/res/layout/item_bottom_sheet_action.xml b/vector/src/main/res/layout/item_bottom_sheet_action.xml
index 0ad7a211da..66a096799d 100644
--- a/vector/src/main/res/layout/item_bottom_sheet_action.xml
+++ b/vector/src/main/res/layout/item_bottom_sheet_action.xml
@@ -38,7 +38,7 @@